4440 lines
172 KiB
C#
4440 lines
172 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.IO;
|
|
using System.Diagnostics;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using AutomationTool;
|
|
using UnrealBuildTool;
|
|
using Ionic.Zip;
|
|
using EpicGames.Core;
|
|
using UnrealBuildBase;
|
|
using System.Text.RegularExpressions;
|
|
using AutomationScripts;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
public class AndroidPlatform : Platform
|
|
{
|
|
// Maximum allowed OBB size (1 GiB, 2 GiB or 4 GiB based on project settings)
|
|
private const Int64 SmallOBBSizeAllowed = 1073741824;
|
|
private const Int64 NormalOBBSizeAllowed = 2147483648;
|
|
private const Int64 MaxOBBSizeAllowed = 4294967296;
|
|
|
|
private const int DeployMaxParallelCommands = 6;
|
|
|
|
private const string TargetAndroidLocation = "obb/";
|
|
private const string TargetAndroidTemp = "/data/local/tmp/";
|
|
|
|
private const string DefaultLaunchActivity = "com.epicgames.unreal.SplashActivity";
|
|
|
|
public class AdbCreatedProcess : AutomationTool.IProcessResult
|
|
{
|
|
private readonly object StopSyncObject = new object();
|
|
IProcessResult AdbLogProcess;
|
|
string LogPath;
|
|
string PackageName;
|
|
string DeviceName;
|
|
int LogFileProcessExitCode = 0;
|
|
bool bStopped = false;
|
|
|
|
public AdbCreatedProcess(
|
|
IProcessResult InAdbLogProcess,
|
|
string InLogPath,
|
|
string InPackageName,
|
|
string InDeviceName)
|
|
{
|
|
AdbLogProcess = InAdbLogProcess;
|
|
LogPath = InLogPath;
|
|
PackageName = InPackageName;
|
|
DeviceName = InDeviceName;
|
|
ProcessManager.AddProcess(this);
|
|
}
|
|
|
|
~AdbCreatedProcess()
|
|
{
|
|
ProcessManager.RemoveProcess(this);
|
|
}
|
|
|
|
public void StopProcess(bool KillDescendants = true)
|
|
{
|
|
lock (StopSyncObject)
|
|
{
|
|
if (!bStopped)
|
|
{
|
|
AndroidPlatform.RunAdbCommand(DeviceName, "shell am force-stop " + PackageName);
|
|
if (!AdbLogProcess.HasExited)
|
|
{
|
|
AdbLogProcess.StopProcess(KillDescendants);
|
|
}
|
|
DumpDeviceOutputToLogFiles();
|
|
bStopped = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool HasExited
|
|
{
|
|
get
|
|
{
|
|
if (!bStopped && (AdbLogProcess.HasExited || !IsPackageRunningOnDevice()))
|
|
{
|
|
StopProcess();
|
|
}
|
|
return bStopped;
|
|
}
|
|
}
|
|
|
|
public string GetProcessName()
|
|
{
|
|
return String.Format("{0}@{1}", PackageName, DeviceName);
|
|
}
|
|
|
|
public void OnProcessExited()
|
|
{
|
|
}
|
|
|
|
public void DisposeProcess()
|
|
{
|
|
AdbLogProcess.DisposeProcess();
|
|
}
|
|
|
|
public void StdOut(object sender, DataReceivedEventArgs e)
|
|
{
|
|
}
|
|
|
|
public void StdErr(object sender, DataReceivedEventArgs e)
|
|
{
|
|
}
|
|
|
|
public int ExitCode
|
|
{
|
|
get { return LogFileProcessExitCode; }
|
|
set { LogFileProcessExitCode = value; }
|
|
}
|
|
|
|
public bool bExitCodeSuccess => ExitCode == 0;
|
|
|
|
public string Output
|
|
{
|
|
get { return AdbLogProcess.Output; }
|
|
}
|
|
|
|
public Process ProcessObject
|
|
{
|
|
get { return AdbLogProcess.ProcessObject; }
|
|
}
|
|
|
|
public void WaitForExit()
|
|
{
|
|
while (!AdbLogProcess.HasExited && IsPackageRunningOnDevice())
|
|
{
|
|
Thread.Sleep(100);
|
|
}
|
|
StopProcess();
|
|
}
|
|
|
|
public FileReference WriteOutputToFile(string FileName)
|
|
{
|
|
return AdbLogProcess.WriteOutputToFile(FileName);
|
|
}
|
|
|
|
private bool IsPackageRunningOnDevice()
|
|
{
|
|
ERunOptions Options = ERunOptions.Default | ERunOptions.SpewIsVerbose | ERunOptions.NoLoggingOfRunCommand;
|
|
IProcessResult Result = AndroidPlatform.RunAdbCommand(DeviceName, "shell ps", null, Options);
|
|
string ProcessList = Result.Output;
|
|
bool bIsProcessRunning = ProcessList.Contains(PackageName);
|
|
return bIsProcessRunning;
|
|
}
|
|
|
|
private void DumpDeviceOutputToLogFiles()
|
|
{
|
|
string SanitizedDeviceName = DeviceName.Replace(":", "_");
|
|
string LogFilename = Path.Combine(LogPath, "devicelog" + SanitizedDeviceName + ".log");
|
|
string ServerLogFilename = Path.Combine(CmdEnv.LogFolder, "devicelog" + SanitizedDeviceName + ".log");
|
|
ERunOptions Options = ERunOptions.Default & ~ERunOptions.AllowSpew;
|
|
IProcessResult LogFileProcess = RunAdbCommand(DeviceName, "logcat -d", null, Options);
|
|
string AllOutput = LogFileProcess.Output;
|
|
File.WriteAllText(LogFilename, AllOutput);
|
|
File.WriteAllText(ServerLogFilename, AllOutput);
|
|
|
|
ExitCode = LogFileProcess.ExitCode;
|
|
}
|
|
}
|
|
|
|
public AndroidPlatform()
|
|
: base(UnrealTargetPlatform.Android)
|
|
{
|
|
|
|
}
|
|
|
|
|
|
public override string[] GetCodeSpecifiedSdkVersions()
|
|
{
|
|
UEBuildPlatformSDK AndroidSDK = UEBuildPlatformSDK.GetSDKForPlatform("Android");
|
|
|
|
return AndroidSDK != null ? new string[] { AndroidSDK.GetMainVersion() } : Array.Empty<string>();
|
|
}
|
|
|
|
// Android has a more complex sdk installation, so perform it manually
|
|
public override bool InstallSDK(BuildCommand BuildCommand, ITurnkeyContext TurnkeyContext, DeviceInfo Device, bool bUnattended, bool bSdkAlreadyInstalled)
|
|
{
|
|
if (Device != null)
|
|
{
|
|
return base.InstallSDK(BuildCommand, TurnkeyContext, Device, bUnattended, bSdkAlreadyInstalled);
|
|
}
|
|
|
|
string SdkDir = GetSdkDir();
|
|
bool bIsInstalled = Directory.Exists(SdkDir);
|
|
|
|
if (!bIsInstalled)
|
|
{
|
|
int Option = 2;
|
|
while (Option == 2)
|
|
{
|
|
string Prompt = $"The Android Sdk directory was not found (expected to find it at '{SdkDir}'\n" +
|
|
"Android Studio can install it for you, but you will need to manually perform some steps (if desired, you can get detailed help with option 2):\n" +
|
|
" - Wait for Android Studio to start, you will see an initial dialog asking how to proceed (called \"Welcome to Android Studio\")\n" +
|
|
" - Click the \"Configure\" dropdown in the bottom right, and select \"SDK Manager\"\n" +
|
|
" - Click on the \"SDK Tools\" tab near the top middle of the right pane\n" +
|
|
" - Check the box next to Android SDK COmmand-line Tools (latest)\n" +
|
|
" - Click OK in the bottom right\n" +
|
|
" - It will probably ask for you to accept a license - you MUST do this\n" +
|
|
" - Once installation has completed, close/quit Android Studio to continue\n";
|
|
|
|
List<string> Options = new()
|
|
{
|
|
"Run Android Studio to install the Command Line Tools",
|
|
"Get detailed step by step guide",
|
|
};
|
|
|
|
Option = TurnkeyContext.ReadInputInt(Prompt, Options, true, 1);
|
|
|
|
if (Option == 0)
|
|
{
|
|
return false;
|
|
}
|
|
if (Option == 2)
|
|
{
|
|
// @todo: we need to redo the documentation DRAMATICALLY on here
|
|
string URL = "https://docs.unrealengine.com/5.1/en-US/how-to-set-up-android-sdk-and-ndk-for-your-unreal-engine-development-environment/";
|
|
Process.Start(new ProcessStartInfo { FileName = URL, UseShellExecute = true });
|
|
}
|
|
}
|
|
|
|
string AndroidStudioExe = GetAndroidStudioExe();
|
|
|
|
if (HostPlatform.Platform == UnrealTargetPlatform.Mac)
|
|
{
|
|
TurnkeyContext.RunExternalCommand("open", $"-W \"{GetAndroidStudioExe()}\"", false, true, true);
|
|
}
|
|
else
|
|
{
|
|
TurnkeyContext.RunExternalCommand(GetAndroidStudioExe(), "", false, true, false);
|
|
}
|
|
}
|
|
|
|
// run the Setup.bat in the engine, not coming from a normal FileSource
|
|
|
|
string Command;
|
|
if (HostPlatform.Platform == UnrealTargetPlatform.Win64)
|
|
{
|
|
Command = "$(EngineDir)/Extras/Android/SetupAndroid.bat";
|
|
}
|
|
else if (HostPlatform.Platform == UnrealTargetPlatform.Mac)
|
|
{
|
|
Command = "$(EngineDir)/Extras/Android/SetupAndroid.command";
|
|
}
|
|
else
|
|
{
|
|
Command = "$(EngineDir)/Extras/Android/SetupAndroid.sh";
|
|
}
|
|
|
|
// pull the desired version numbers to install
|
|
UEBuildPlatformSDK AndroidSDK = UEBuildPlatformSDK.GetSDKForPlatform("Android");
|
|
string PlatformsVersion = AndroidSDK.GetPlatformSpecificVersion("platforms");
|
|
string BuildToolsVersion = AndroidSDK.GetPlatformSpecificVersion("build-tools");
|
|
string CMakeVersion = AndroidSDK.GetPlatformSpecificVersion("cmake");
|
|
string NDKVersion = AndroidSDK.GetPlatformSpecificVersion("ndk");
|
|
|
|
string Params = $"{PlatformsVersion} {BuildToolsVersion} {CMakeVersion} {NDKVersion} -noninteractive";
|
|
|
|
// because this may bring up a license acceptance message that needs the user to respond, so we make a new window
|
|
int ExitCode = TurnkeyContext.RunExternalCommand(Command, Params, bRequiresPrivilegeElevation: false, bUnattended, bCreateWindow: true);
|
|
return ExitCode == 0;
|
|
}
|
|
|
|
public override bool PostSDKSetup(ITurnkeyContext TurnkeyContext, bool bUnattended)
|
|
{
|
|
FileReference EmulatorConfigFile = FileReference.Combine(Unreal.EngineDirectory, "Platforms", "Android", "Config", "AndroidEmulator.json");
|
|
|
|
if (!FileReference.Exists(EmulatorConfigFile))
|
|
{
|
|
EmulatorConfigFile = FileReference.Combine(Unreal.EngineDirectory, "Config", "Android", "AndroidEmulator.json");
|
|
|
|
if (!FileReference.Exists(EmulatorConfigFile))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
Dictionary<string, string> EmulatorConfig = JsonSerializer.Deserialize<Dictionary<string, string>>(FileReference.ReadAllText(EmulatorConfigFile), new JsonSerializerOptions
|
|
{
|
|
AllowTrailingCommas = true,
|
|
ReadCommentHandling = JsonCommentHandling.Skip
|
|
});
|
|
|
|
string CommandExtension = HostPlatform.Platform == UnrealTargetPlatform.Win64 ? "bat" : HostPlatform.Platform == UnrealTargetPlatform.Mac ? "command" : "sh";
|
|
|
|
// run the SetupAndroidEmulator in the engine, not coming from a normal FileSource
|
|
string Command = $"$(EngineDir)/Extras/Android/SetupAndroidEmulator.{CommandExtension}";
|
|
string Params = $"{EmulatorConfig["AvdDevice"]} {EmulatorConfig["DeviceApiVersion"]} {EmulatorConfig["DeviceDataSize"]} {EmulatorConfig["DeviceRamSize"]} -noninteractive";
|
|
|
|
// because this may bring up a license acceptance message that needs the user to respond, so we make a new window
|
|
int ExitCode = TurnkeyContext.RunExternalCommand(Command, Params, bRequiresPrivilegeElevation: false, bUnattended, bCreateWindow: true);
|
|
|
|
return ExitCode == 0;
|
|
}
|
|
|
|
private static string GetAndroidStudioExe()
|
|
{
|
|
if (OperatingSystem.IsLinux())
|
|
{
|
|
string UserHome = Environment.GetEnvironmentVariable("HOME");
|
|
string AndroidStudioExe = Path.Combine(UserHome, "android-studio", "bin", "studio.sh");
|
|
|
|
return AndroidStudioExe;
|
|
}
|
|
else if (OperatingSystem.IsMacOS())
|
|
{
|
|
|
|
string AndroidStudioExe = "/Applications/Android Studio.app";
|
|
if (Directory.Exists(AndroidStudioExe))
|
|
{
|
|
return AndroidStudioExe;
|
|
}
|
|
|
|
string UserHome = Environment.GetEnvironmentVariable("HOME");
|
|
AndroidStudioExe = Path.Combine(UserHome, "Applications", "Android Studio.app");
|
|
|
|
return AndroidStudioExe;
|
|
}
|
|
|
|
Debug.Assert(OperatingSystem.IsWindows());
|
|
|
|
string DefaultAndroidStudioInstallDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Android", "Android Studio");
|
|
string RegValue = Microsoft.Win32.Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Android Studio", "Path", null) as string;
|
|
string AndroidStudioInstallDir = RegValue == null ? DefaultAndroidStudioInstallDir : RegValue;
|
|
// Some installs, like JetBrains Toolbox, may not place an entry in the registry so try an alternate location
|
|
AndroidStudioInstallDir = Directory.Exists(AndroidStudioInstallDir) ? AndroidStudioInstallDir : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "Android Studio");
|
|
return Path.Combine(AndroidStudioInstallDir, "bin", "studio64.exe");
|
|
}
|
|
|
|
private static string GetSdkDir()
|
|
{
|
|
string AndroidHome = Environment.GetEnvironmentVariable("ANDROID_HOME");
|
|
if (!string.IsNullOrEmpty(AndroidHome) && Directory.Exists(AndroidHome))
|
|
{
|
|
return AndroidHome;
|
|
}
|
|
|
|
if (OperatingSystem.IsLinux())
|
|
{
|
|
string UserHome = Environment.GetEnvironmentVariable("HOME");
|
|
string AndroidSdkPath = Path.Combine(UserHome, "Android", "sdk");
|
|
|
|
return AndroidSdkPath;
|
|
}
|
|
else if (OperatingSystem.IsMacOS())
|
|
{
|
|
string BashProfilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), ".bash_profile");
|
|
if (!File.Exists(BashProfilePath))
|
|
{
|
|
// Try .bashrc if didn't fine .bash_profile
|
|
BashProfilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), ".bashrc");
|
|
}
|
|
if (File.Exists(BashProfilePath))
|
|
{
|
|
string[] BashProfileContents = File.ReadAllLines(BashProfilePath);
|
|
|
|
// Walk backwards so we keep the last export setting instead of the first
|
|
string SdkKey = "ANDROID_HOME";
|
|
for (int LineIndex = BashProfileContents.Length - 1; LineIndex >= 0; --LineIndex)
|
|
{
|
|
if (BashProfileContents[LineIndex].StartsWith("export " + SdkKey + "="))
|
|
{
|
|
string PathVar = BashProfileContents[LineIndex].Split('=')[1].Replace("\"", "");
|
|
return PathVar;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
string UserHome = Environment.GetEnvironmentVariable("HOME");
|
|
string AndroidSdkPath = Path.Combine(UserHome, "Library", "Android", "sdk");
|
|
|
|
return AndroidSdkPath;
|
|
}
|
|
|
|
Debug.Assert(OperatingSystem.IsWindows());
|
|
|
|
string DefaultSdkDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Android", "Sdk");
|
|
string RegValue = Microsoft.Win32.Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Android Studio", "SdkPath", null) as string;
|
|
return RegValue == null ? DefaultSdkDir : RegValue;
|
|
}
|
|
|
|
public override bool UpdateHostPrerequisites(BuildCommand Command, ITurnkeyContext TurnkeyContext, bool bVerifyOnly)
|
|
{
|
|
string AndroidStudioExe = GetAndroidStudioExe();
|
|
|
|
bool bIsMac = (HostPlatform.Current.HostEditorPlatform == UnrealTargetPlatform.Mac);
|
|
bool bHaveAndroidStudio = (bIsMac && Directory.Exists(AndroidStudioExe)) ||
|
|
(!bIsMac && FileExists(AndroidStudioExe));
|
|
|
|
// if we are only verifying, just return the status, and if it's installed, we are done!
|
|
if (bVerifyOnly)
|
|
{
|
|
if (!bHaveAndroidStudio)
|
|
{
|
|
TurnkeyContext.ReportError("Android Studio is not installed correctly.");
|
|
}
|
|
return bHaveAndroidStudio;
|
|
}
|
|
|
|
if (!bHaveAndroidStudio)
|
|
{
|
|
TurnkeyContext.PauseForUser("Android Studio was not found on this machine. Press Enter to download and install Android Studio which is required to use Android.");
|
|
|
|
// get AS installer
|
|
string OutputPath = TurnkeyContext.RetrieveFileSource("AndroidStudio");
|
|
|
|
// Unset some envvars in case autosdk ran - they will mess up the first run of Android Studio
|
|
string[] Vars = new string[]
|
|
{
|
|
"ANDROID_HOME",
|
|
"ANDROID_SDK_HOME",
|
|
"JAVA_HOME",
|
|
"NDKROOT",
|
|
"NDK_ROOT",
|
|
"ANDROID_NDK_ROOT",
|
|
"ANDROID_SWT"
|
|
};
|
|
Array.ForEach(Vars, x => Environment.SetEnvironmentVariable(x, null));
|
|
|
|
if (OutputPath == null)
|
|
{
|
|
TurnkeyContext.PauseForUser("Unable to find Android Studio installer. Please download and install Android Studio 2022.2.1 from https://developer.android.com/studio/archive to standard location before continuing.");
|
|
}
|
|
else
|
|
{
|
|
if (HostPlatform.Current.HostEditorPlatform == UnrealTargetPlatform.Linux)
|
|
{
|
|
// TODO finish GUI support for Linux here, otherwise this will throw. Using zenity
|
|
// TurnkeyContext.PauseForUser("Running the Android Studio installer, and then Android Studio for first-time setup!\n\nChoose all default options unless you know what you are doing.");
|
|
|
|
string UserHome = Environment.GetEnvironmentVariable("HOME");
|
|
string Args = string.Format("-xf {0} -C {1}", OutputPath, UserHome);
|
|
|
|
int ExitCode = TurnkeyContext.RunExternalCommand("/bin/tar", Args, false, true, true);
|
|
|
|
if (ExitCode != 0)
|
|
{
|
|
TurnkeyContext.ReportError($"Android Studio installer failed. ExitCode = {ExitCode}");
|
|
return false;
|
|
}
|
|
}
|
|
else if (HostPlatform.Current.HostEditorPlatform == UnrealTargetPlatform.Mac)
|
|
{
|
|
|
|
string UserHome = Environment.GetEnvironmentVariable("HOME");
|
|
string SourceApp = Path.Combine(OutputPath, "Android Studio.app");
|
|
|
|
int ExitCode = TurnkeyContext.RunExternalCommand("/usr/bin/hdiutil", "attach " + OutputPath, false, true, true);
|
|
|
|
if (ExitCode != 0)
|
|
{
|
|
TurnkeyContext.ReportError($"Android Studio installer failed. ExitCode = {ExitCode}");
|
|
return false;
|
|
}
|
|
|
|
string AndroidStudioVolume = "";
|
|
foreach (string Volume in Directory.GetDirectories("/Volumes"))
|
|
|
|
{
|
|
if (Volume.Contains("Android Studio"))
|
|
{
|
|
AndroidStudioVolume = Volume;
|
|
break;
|
|
}
|
|
}
|
|
if (AndroidStudioVolume == "")
|
|
{
|
|
TurnkeyContext.ReportError($"Android Studio installer failed. DMG did not mount");
|
|
return false;
|
|
}
|
|
|
|
string SourcePath = Path.Combine(AndroidStudioVolume, "Android Studio.app");
|
|
string DestPath = Path.Combine(UserHome, "Applications") + "/";
|
|
if (SourcePath.Contains(" "))
|
|
{
|
|
SourcePath = "\"" + SourcePath + "\"";
|
|
}
|
|
if (DestPath.Contains(" "))
|
|
{
|
|
DestPath = "\"" + DestPath + "\"";
|
|
}
|
|
|
|
ExitCode = TurnkeyContext.RunExternalCommand("/bin/cp", "-R " + SourcePath + " " + DestPath, false, true, true);
|
|
|
|
if (AndroidStudioVolume.Contains(" "))
|
|
{
|
|
AndroidStudioVolume = "\"" + AndroidStudioVolume + "\"";
|
|
}
|
|
int ExitCode2 = TurnkeyContext.RunExternalCommand("/usr/bin/hdiutil", "detach " + AndroidStudioVolume, false, true, true);
|
|
|
|
// give error for cp, but can ignore detach failure
|
|
if (ExitCode != 0)
|
|
{
|
|
TurnkeyContext.ReportError($"Android Studio installer failed. ExitCode = {ExitCode}");
|
|
return false;
|
|
}
|
|
}
|
|
else if (HostPlatform.Current.HostEditorPlatform == UnrealTargetPlatform.Win64)
|
|
{
|
|
// install AS with the /S switch
|
|
int ExitCode = TurnkeyContext.RunExternalCommand(OutputPath, "/S", false, true, true);
|
|
|
|
// AS installer returns 1223 even on success ("user canceled" even tho there's no UI to cancel it...) when running with /S
|
|
if (ExitCode != 0 && ExitCode != 1223)
|
|
{
|
|
TurnkeyContext.ReportError($"Android Studio installer failed. ExitCode = {ExitCode}");
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TurnkeyContext.ReportError($"Invalid host platform");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// check to see if the installation worked. If so, continue on!
|
|
AndroidStudioExe = GetAndroidStudioExe();
|
|
|
|
bHaveAndroidStudio = (bIsMac && Directory.Exists(AndroidStudioExe)) ||
|
|
(!bIsMac && FileExists(AndroidStudioExe));
|
|
|
|
if (!bHaveAndroidStudio)
|
|
{
|
|
TurnkeyContext.ReportError("Android Studio is not installed correctly, after attempted installation.");
|
|
}
|
|
|
|
return bHaveAndroidStudio;
|
|
}
|
|
|
|
static string GetAvdHomePath()
|
|
{
|
|
return Environment.GetEnvironmentVariable("ANDROID_AVD_HOME") ?? Path.Combine(Environment.GetEnvironmentVariable("ANDROID_EMULATOR_HOME") ?? Environment.GetEnvironmentVariable("ANDROID_USER_HOME") ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".android"), "avd");
|
|
}
|
|
|
|
public override DeviceInfo[] GetDevices()
|
|
{
|
|
List<DeviceInfo> Devices = new List<DeviceInfo>();
|
|
|
|
List<string> ConnectedDevices;
|
|
GetConnectedDevices(null, out ConnectedDevices);
|
|
|
|
foreach (string ConnectedDeviceName in ConnectedDevices)
|
|
{
|
|
DeviceInfo CurrentDevice = new DeviceInfo(TargetPlatformType);
|
|
// GetConnectedDevices returns valid device names with an '@' in front
|
|
CurrentDevice.Name = ConnectedDeviceName.StartsWith("@") ? ConnectedDeviceName.Remove(0, 1) : ConnectedDeviceName;
|
|
|
|
CurrentDevice.Id = CurrentDevice.Name;
|
|
CurrentDevice.bCanConnect = ConnectedDeviceName.StartsWith("@");
|
|
|
|
// Instead, we return the SDK Version, and other parts of the code were adjusted
|
|
string GetPropCommand = "shell getprop";
|
|
string SDKVersionCommand = $"{GetPropCommand} ro.build.version.sdk";
|
|
IProcessResult Result = RunAdbCommand(CurrentDevice.Name, SDKVersionCommand);
|
|
if (Result.Output.Length > 0)
|
|
{
|
|
CurrentDevice.SoftwareVersion = Result.Output.Trim();
|
|
}
|
|
|
|
Devices.Add(CurrentDevice);
|
|
}
|
|
return Devices.ToArray();
|
|
}
|
|
|
|
public override DeviceInfo GetDeviceByName(string DeviceName)
|
|
{
|
|
if (DeviceName.StartsWith("avd-"))
|
|
{
|
|
return Directory.EnumerateFiles(GetAvdHomePath(), "*.ini").Select(Filename =>
|
|
{
|
|
string AvdName = "avd-" + Path.GetFileNameWithoutExtension(Filename);
|
|
string Target = GetIniValue(Filename, "target");
|
|
const string TargetPrefix = "android-";
|
|
string SdkVersion = Target.StartsWith(TargetPrefix) ? Target[TargetPrefix.Length..] : "";
|
|
|
|
return new DeviceInfo(TargetPlatformType)
|
|
{
|
|
Id = AvdName,
|
|
Name = AvdName,
|
|
bCanConnect = true,
|
|
SoftwareVersion = SdkVersion.Length > 0 ? SdkVersion : null
|
|
};
|
|
}).FirstOrDefault(Device => string.Compare(Device.Id, DeviceName, true) == 0);
|
|
}
|
|
else
|
|
{
|
|
return base.GetDeviceByName(DeviceName);
|
|
}
|
|
}
|
|
|
|
private static string GetSONameWithoutArchitecture(ProjectParams Params, string DecoratedExeName)
|
|
{
|
|
return Path.Combine(Path.GetDirectoryName(Params.GetProjectExeForPlatform(UnrealTargetPlatform.Android).ToString()), DecoratedExeName) + ".so";
|
|
}
|
|
|
|
private static string GetSOName(ProjectParams Params, string DecoratedExeName, UnrealArch? Architecture)
|
|
{
|
|
string ArchName = Architecture == null ? "" : "-" + Architecture.ToString();
|
|
return Path.Combine(Path.GetDirectoryName(Params.GetProjectExeForPlatform(UnrealTargetPlatform.Android).ToString()), DecoratedExeName) + ArchName + ".so";
|
|
}
|
|
|
|
private static string GetFinalApkName(ProjectParams Params, string DecoratedExeName, bool bRenameUnrealGame, UnrealArch? Architecture)
|
|
{
|
|
string ProjectDir = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(Params.RawProjectPath.FullName)), "Binaries/Android");
|
|
|
|
if (Params.Prebuilt)
|
|
{
|
|
ProjectDir = Path.Combine(Params.BaseStageDirectory, "Android");
|
|
}
|
|
|
|
// Apk's go to project location, not necessarily where the .so is (content only packages need to output to their directory)
|
|
string ArchName = Architecture == null ? "" : "-" + Architecture.ToString();
|
|
string ApkName = Path.Combine(ProjectDir, $"{DecoratedExeName}{ArchName}.apk");
|
|
|
|
// if the source binary was UnrealGame, handle using it or switching to project name
|
|
if (Path.GetFileNameWithoutExtension(Params.GetProjectExeForPlatform(UnrealTargetPlatform.Android).ToString()) == "UnrealGame")
|
|
{
|
|
if (bRenameUnrealGame)
|
|
{
|
|
// replace UnrealGame with project name (only replace in the filename part)
|
|
ApkName = Path.Combine(Path.GetDirectoryName(ApkName), Path.GetFileName(ApkName).Replace("UnrealGame", Params.ShortProjectName));
|
|
}
|
|
else
|
|
{
|
|
// if we want to use UE directly then use it from the engine directory not project directory
|
|
ApkName = ApkName.Replace(ProjectDir, Path.Combine(CmdEnv.LocalRoot, "Engine/Binaries/Android"));
|
|
}
|
|
}
|
|
|
|
return ApkName;
|
|
}
|
|
|
|
private static bool bHaveReadEngineVersion = false;
|
|
private static string EngineMajorVersion = "4";
|
|
private static string EngineMinorVersion = "0";
|
|
private static string EnginePatchVersion = "0";
|
|
|
|
#pragma warning disable CS0414
|
|
private static string EngineChangelist = "0";
|
|
|
|
private static string ReadEngineVersion(string EngineDirectory)
|
|
{
|
|
if (!bHaveReadEngineVersion)
|
|
{
|
|
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(' ');
|
|
}
|
|
else if (EngineVersionLines[i].StartsWith("#define BUILT_FROM_CHANGELIST"))
|
|
{
|
|
EngineChangelist = EngineVersionLines[i].Split(new char[] { ' ', '\t' })[2].Trim(' ');
|
|
}
|
|
}
|
|
|
|
bHaveReadEngineVersion = true;
|
|
}
|
|
|
|
return EngineMajorVersion + "." + EngineMinorVersion + "." + EnginePatchVersion;
|
|
}
|
|
|
|
#pragma warning restore CS0414
|
|
|
|
|
|
private static string GetFinalSymbolizedSODirectory(string ApkName, DeploymentContext SC, UnrealArch Architecture)
|
|
{
|
|
string PackageVersion = GetPackageInfo(ApkName, SC, true);
|
|
if (PackageVersion == null || PackageVersion.Length == 0)
|
|
{
|
|
throw new AutomationException(ExitCode.Error_FailureGettingPackageInfo, "Failed to get package version from " + ApkName);
|
|
}
|
|
|
|
return SC.ShortProjectName + "_Symbols_v" + PackageVersion + "/" + SC.ShortProjectName + Architecture;
|
|
}
|
|
|
|
private static string GetFinalObbName(string ApkName, DeploymentContext SC, bool bUseAppType = true)
|
|
{
|
|
// calculate the name for the .obb file
|
|
string PackageName = GetPackageInfo(ApkName, SC, false);
|
|
if (PackageName == null)
|
|
{
|
|
throw new AutomationException(ExitCode.Error_FailureGettingPackageInfo, "Failed to get package name from " + ApkName);
|
|
}
|
|
|
|
string PackageVersion = GetPackageInfo(ApkName, SC, true);
|
|
if (PackageVersion == null || PackageVersion.Length == 0)
|
|
{
|
|
throw new AutomationException(ExitCode.Error_FailureGettingPackageInfo, "Failed to get package version from " + ApkName);
|
|
}
|
|
|
|
if (PackageVersion.Length > 0)
|
|
{
|
|
int IntVersion = int.Parse(PackageVersion);
|
|
PackageVersion = IntVersion.ToString("0");
|
|
}
|
|
|
|
string AppType = bUseAppType ? GetMetaAppType() : "";
|
|
if (AppType.Length > 0)
|
|
{
|
|
AppType += ".";
|
|
}
|
|
|
|
string ObbName = string.Format("main.{0}.{1}.{2}obb", PackageVersion, PackageName, AppType);
|
|
|
|
// plop the .obb right next to the executable
|
|
ObbName = Path.Combine(Path.GetDirectoryName(ApkName), ObbName);
|
|
|
|
return ObbName;
|
|
}
|
|
|
|
private static string GetFinalPatchName(string ApkName, DeploymentContext SC, bool bUseAppType = true)
|
|
{
|
|
// calculate the name for the .obb file
|
|
string PackageName = GetPackageInfo(ApkName, SC, false);
|
|
if (PackageName == null)
|
|
{
|
|
throw new AutomationException(ExitCode.Error_FailureGettingPackageInfo, "Failed to get package name from " + ApkName);
|
|
}
|
|
|
|
string PackageVersion = GetPackageInfo(ApkName, SC, true);
|
|
if (PackageVersion == null || PackageVersion.Length == 0)
|
|
{
|
|
throw new AutomationException(ExitCode.Error_FailureGettingPackageInfo, "Failed to get package version from " + ApkName);
|
|
}
|
|
|
|
if (PackageVersion.Length > 0)
|
|
{
|
|
int IntVersion = int.Parse(PackageVersion);
|
|
PackageVersion = IntVersion.ToString("0");
|
|
}
|
|
|
|
string AppType = bUseAppType ? GetMetaAppType() : "";
|
|
if (AppType.Length > 0)
|
|
{
|
|
AppType += ".";
|
|
}
|
|
|
|
string PatchName = string.Format("patch.{0}.{1}.{2}obb", PackageVersion, PackageName, AppType);
|
|
|
|
// plop the .obb right next to the executable
|
|
PatchName = Path.Combine(Path.GetDirectoryName(ApkName), PatchName);
|
|
|
|
return PatchName;
|
|
}
|
|
|
|
private static string GetFinalOverflowName(string ApkName, DeploymentContext SC, int Index, bool bUseAppType = true)
|
|
{
|
|
// calculate the name for the .obb file
|
|
string PackageName = GetPackageInfo(ApkName, SC, false);
|
|
if (PackageName == null)
|
|
{
|
|
throw new AutomationException(ExitCode.Error_FailureGettingPackageInfo, "Failed to get package name from " + ApkName);
|
|
}
|
|
|
|
string PackageVersion = GetPackageInfo(ApkName, SC, true);
|
|
if (PackageVersion == null || PackageVersion.Length == 0)
|
|
{
|
|
throw new AutomationException(ExitCode.Error_FailureGettingPackageInfo, "Failed to get package version from " + ApkName);
|
|
}
|
|
|
|
if (PackageVersion.Length > 0)
|
|
{
|
|
int IntVersion = int.Parse(PackageVersion);
|
|
PackageVersion = IntVersion.ToString("0");
|
|
}
|
|
|
|
string AppType = bUseAppType ? GetMetaAppType() : "";
|
|
if (AppType.Length > 0)
|
|
{
|
|
AppType += ".";
|
|
}
|
|
|
|
string OverflowName = string.Format("overflow{0}.{1}.{2}.{3}obb", Index, PackageVersion, PackageName, AppType);
|
|
|
|
// plop the .obb right next to the executable
|
|
OverflowName = Path.Combine(Path.GetDirectoryName(ApkName), OverflowName);
|
|
|
|
return OverflowName;
|
|
}
|
|
|
|
|
|
public override string GetPlatformPakCommandLine(ProjectParams Params, DeploymentContext SC)
|
|
{
|
|
string PakParams = " -patchpaddingalign=0";
|
|
|
|
string OodleDllPath = DirectoryReference.Combine(SC.ProjectRoot, "Binaries/ThirdParty/Oodle/Win64/UnrealPakPlugin.dll").FullName;
|
|
if (File.Exists(OodleDllPath))
|
|
{
|
|
PakParams += String.Format(" -customcompressor=\"{0}\"", OodleDllPath);
|
|
}
|
|
|
|
return PakParams;
|
|
}
|
|
|
|
private static string GetDeviceObbName(string ApkName, DeploymentContext SC)
|
|
{
|
|
string ObbName = GetFinalObbName(ApkName, SC, false);
|
|
string PackageName = GetPackageInfo(ApkName, SC, false);
|
|
return TargetAndroidLocation + PackageName + "/" + Path.GetFileName(ObbName);
|
|
}
|
|
|
|
private static string GetDevicePatchName(string ApkName, DeploymentContext SC)
|
|
{
|
|
string PatchName = GetFinalPatchName(ApkName, SC, false);
|
|
string PackageName = GetPackageInfo(ApkName, SC, false);
|
|
return TargetAndroidLocation + PackageName + "/" + Path.GetFileName(PatchName);
|
|
}
|
|
|
|
private static string GetDeviceOverflowName(string ApkName, DeploymentContext SC, int Index)
|
|
{
|
|
string OverflowName = GetFinalOverflowName(ApkName, SC, Index, false);
|
|
string PackageName = GetPackageInfo(ApkName, SC, false);
|
|
return TargetAndroidLocation + PackageName + "/" + Path.GetFileName(OverflowName);
|
|
}
|
|
|
|
public static string GetStorageQueryCommand(bool bForcePC = false)
|
|
{
|
|
if (!bForcePC && !OperatingSystem.IsWindows())
|
|
{
|
|
return "shell 'echo $EXTERNAL_STORAGE'";
|
|
}
|
|
else
|
|
{
|
|
return "shell \"echo $EXTERNAL_STORAGE\"";
|
|
}
|
|
}
|
|
|
|
enum EBatchType
|
|
{
|
|
Install,
|
|
Uninstall,
|
|
Symbolize,
|
|
};
|
|
private static string GetFinalBatchName(string ApkName, DeploymentContext SC, bool bNoOBBInstall, EBatchType BatchType, UnrealTargetPlatform Target)
|
|
{
|
|
string Extension = ".bat";
|
|
if (Target == UnrealTargetPlatform.Linux || Target == UnrealTargetPlatform.LinuxArm64)
|
|
{
|
|
Extension = ".sh";
|
|
}
|
|
else if (Target == UnrealTargetPlatform.Mac)
|
|
{
|
|
Extension = ".command";
|
|
}
|
|
|
|
// Get the name of the APK to use for batch file
|
|
string ExecutableName = Path.GetFileNameWithoutExtension(ApkName);
|
|
|
|
switch(BatchType)
|
|
{
|
|
case EBatchType.Install:
|
|
case EBatchType.Uninstall:
|
|
return Path.Combine(Path.GetDirectoryName(ApkName), (BatchType == EBatchType.Uninstall ? "Uninstall_" : "Install_") + ExecutableName + (!bNoOBBInstall ? "" : "_NoOBBInstall") + Extension);
|
|
case EBatchType.Symbolize:
|
|
return Path.Combine(Path.GetDirectoryName(ApkName), "SymbolizeCrashDump_" + ExecutableName + Extension);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
private UnrealArchitectures GetDeploymentArchitectures(ProjectParams Params, DeploymentContext SC)
|
|
{
|
|
return Params.ClientArchitecture ?? SC.StageTargets[0].Receipt.Architectures;
|
|
}
|
|
|
|
private List<string> CollectPluginDataPaths(DeploymentContext SC)
|
|
{
|
|
// collect plugin extra data paths from target receipts
|
|
List<string> PluginExtras = new List<string>();
|
|
foreach (StageTarget Target in SC.StageTargets)
|
|
{
|
|
TargetReceipt Receipt = 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);
|
|
Logger.LogInformation("AndroidPlugin: {PluginPath}", PluginPath);
|
|
}
|
|
}
|
|
}
|
|
return PluginExtras;
|
|
}
|
|
|
|
private bool UsingAndroidFileServer(ProjectParams Params, DeploymentContext SC, out bool bEnablePlugin, out string AFSToken, out bool bIsShipping, out bool bIncludeInShipping, out bool bAllowExternalStartInShipping)
|
|
{
|
|
FileReference RawProjectPath = SC != null ? SC.RawProjectPath : Params.RawProjectPath;
|
|
UnrealTargetPlatform TargetPlatform = SC != null ? SC.StageTargetPlatform.PlatformType : Params.ClientTargetPlatforms[0].Type;
|
|
UnrealTargetConfiguration TargetConfiguration = SC != null ? SC.StageTargetConfigurations[0] : Params.ClientConfigsToBuild[0];
|
|
bIsShipping = TargetConfiguration == UnrealTargetConfiguration.Shipping;
|
|
|
|
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(RawProjectPath), TargetPlatform);
|
|
if (!Ini.GetBool("/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings", "bEnablePlugin", out bEnablePlugin))
|
|
{
|
|
bEnablePlugin = true;
|
|
}
|
|
if (!Ini.GetString("/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings", "SecurityToken", out AFSToken))
|
|
{
|
|
AFSToken = "";
|
|
}
|
|
if (!Ini.GetBool("/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings", "bIncludeInShipping", out bIncludeInShipping))
|
|
{
|
|
bIncludeInShipping = false;
|
|
}
|
|
if (!Ini.GetBool("/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings", "bAllowExternalStartInShipping", out bAllowExternalStartInShipping))
|
|
{
|
|
bAllowExternalStartInShipping = false;
|
|
}
|
|
|
|
if (bIsShipping && !(bIncludeInShipping && bAllowExternalStartInShipping))
|
|
{
|
|
return false;
|
|
}
|
|
return bEnablePlugin;
|
|
}
|
|
|
|
enum EConnectionType
|
|
{
|
|
USBOnly,
|
|
NetworkOnly,
|
|
Combined
|
|
}
|
|
|
|
private EConnectionType GetAndroidFileServerNetworkConfig(DeploymentContext SC, out bool bUseCompression, out bool bLogFiles, out bool bReportStats, out bool bUseManualIPAddress, out string ManualIPAddress)
|
|
{
|
|
EConnectionType ConnectionType = EConnectionType.USBOnly;
|
|
|
|
UnrealTargetConfiguration TargetConfiguration = SC.StageTargetConfigurations[0];
|
|
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(SC.RawProjectPath), SC.StageTargetPlatform.PlatformType);
|
|
|
|
string ConnectionString = "";
|
|
Ini.GetString("/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings", "ConnectionType", out ConnectionString);
|
|
switch (ConnectionString)
|
|
{
|
|
case "USBOnly":
|
|
ConnectionType = EConnectionType.USBOnly;
|
|
break;
|
|
case "NetworkOnly":
|
|
ConnectionType = EConnectionType.NetworkOnly;
|
|
break;
|
|
case "Combined":
|
|
ConnectionType = EConnectionType.Combined;
|
|
break;
|
|
default:
|
|
ConnectionType = EConnectionType.USBOnly;
|
|
break;
|
|
}
|
|
|
|
if (!Ini.GetBool("/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings", "bUseCompression", out bUseCompression))
|
|
{
|
|
bUseCompression = false;
|
|
}
|
|
if (!Ini.GetBool("/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings", "bLogFiles", out bLogFiles))
|
|
{
|
|
bLogFiles = false;
|
|
}
|
|
if (!Ini.GetBool("/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings", "bReportStats", out bReportStats))
|
|
{
|
|
bReportStats = false;
|
|
}
|
|
if (!Ini.GetBool("/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings", "bUseManualIPAddress", out bUseManualIPAddress))
|
|
{
|
|
bUseManualIPAddress = false;
|
|
}
|
|
if (!Ini.GetString("/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings", "ManualIPAddress", out ManualIPAddress))
|
|
{
|
|
ManualIPAddress = "127.0.0.1";
|
|
}
|
|
|
|
bool bAllowNetworkConnection = true;
|
|
if (!Ini.GetBool("/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings", "bAllowNetworkConnection", out bAllowNetworkConnection))
|
|
{
|
|
bAllowNetworkConnection = true;
|
|
}
|
|
if (!bAllowNetworkConnection && ConnectionType != EConnectionType.USBOnly)
|
|
{
|
|
Logger.LogWarning("AFS will only use USB connection due to network connection disabled");
|
|
ConnectionType = EConnectionType.USBOnly;
|
|
}
|
|
|
|
return ConnectionType;
|
|
}
|
|
|
|
private bool BuildWithHiddenSymbolVisibility(DeploymentContext SC)
|
|
{
|
|
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(SC.RawProjectPath), SC.StageTargetPlatform.PlatformType);
|
|
return Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bBuildWithHiddenSymbolVisibility", out bool bBuild) && bBuild;
|
|
}
|
|
|
|
private bool GetSaveSymbols(DeploymentContext SC)
|
|
{
|
|
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(SC.RawProjectPath), SC.StageTargetPlatform.PlatformType);
|
|
bool bSave = false;
|
|
return (Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bSaveSymbols", out bSave) && bSave);
|
|
}
|
|
|
|
private bool GetEnableBundle(DeploymentContext SC)
|
|
{
|
|
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(SC.RawProjectPath), SC.StageTargetPlatform.PlatformType);
|
|
bool bEnableBundle = false;
|
|
return (Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bEnableBundle", out bEnableBundle) && bEnableBundle);
|
|
}
|
|
|
|
private bool GetEnableUniversalAPK(DeploymentContext SC)
|
|
{
|
|
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(SC.RawProjectPath), SC.StageTargetPlatform.PlatformType);
|
|
bool bEnableUniversalAPK = false;
|
|
return (Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bEnableUniversalAPK", out bEnableUniversalAPK) && bEnableUniversalAPK);
|
|
}
|
|
|
|
private Int64 GetMaxOBBSizeAllowed(DeploymentContext SC)
|
|
{
|
|
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(SC.RawProjectPath), SC.StageTargetPlatform.PlatformType);
|
|
bool bForceSmallOBBFiles = false;
|
|
bool bAllowLargeOBBFiles = false;
|
|
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bForceSmallOBBFiles", out bForceSmallOBBFiles);
|
|
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bAllowLargeOBBFiles", out bAllowLargeOBBFiles);
|
|
return bForceSmallOBBFiles ? SmallOBBSizeAllowed : (bAllowLargeOBBFiles ? MaxOBBSizeAllowed : NormalOBBSizeAllowed);
|
|
}
|
|
|
|
private bool AllowPatchOBBFile(DeploymentContext SC)
|
|
{
|
|
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(SC.RawProjectPath), SC.StageTargetPlatform.PlatformType);
|
|
bool bAllowPatchOBBFile = false;
|
|
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bAllowPatchOBBFile", out bAllowPatchOBBFile);
|
|
return bAllowPatchOBBFile;
|
|
}
|
|
|
|
private int AllowOverflowOBBFiles(DeploymentContext SC)
|
|
{
|
|
int FileLimit = 0;
|
|
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(SC.RawProjectPath), SC.StageTargetPlatform.PlatformType);
|
|
bool bAllowOverflowOBBFiles = false;
|
|
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bAllowOverflowOBBFiles", out bAllowOverflowOBBFiles);
|
|
if (bAllowOverflowOBBFiles)
|
|
{
|
|
FileLimit = 2;
|
|
Ini.GetInt32("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "OverflowOBBFileLimit", out FileLimit);
|
|
}
|
|
return FileLimit;
|
|
}
|
|
|
|
private bool CreateOBBFile(DeploymentContext SC, string OutputFilename, List<FileReference> FilesForObb)
|
|
{
|
|
Logger.LogInformation("Creating {OutputFilename} from {Arg1}", OutputFilename, SC.StageDirectory);
|
|
using (ZipFile ObbFile = new ZipFile(OutputFilename))
|
|
{
|
|
ObbFile.CompressionMethod = CompressionMethod.None;
|
|
ObbFile.CompressionLevel = Ionic.Zlib.CompressionLevel.None;
|
|
ObbFile.UseZip64WhenSaving = Ionic.Zip.Zip64Option.Never;
|
|
ObbFile.Comment = String.Format("{0,10}", "1");
|
|
|
|
int ObbFileCount = 0;
|
|
ObbFile.AddProgress +=
|
|
delegate (object sender, AddProgressEventArgs e)
|
|
{
|
|
if (e.EventType == ZipProgressEventType.Adding_AfterAddEntry)
|
|
{
|
|
ObbFileCount += 1;
|
|
Logger.LogInformation("[{Count}/{Total}] Adding {File} to OBB",
|
|
ObbFileCount, e.EntriesTotal,
|
|
e.CurrentEntry.FileName);
|
|
}
|
|
};
|
|
|
|
foreach (FileReference FileRef in FilesForObb)
|
|
{
|
|
string DestinationDirectoryPath = Path.GetRelativePath(SC.StageDirectory.FullName, Path.GetDirectoryName(FileRef.FullName));
|
|
ObbFile.AddFile(FileRef.FullName, DestinationDirectoryPath);
|
|
}
|
|
|
|
// ObbFile.AddDirectory(SC.StageDirectory+"/"+SC.ShortProjectName, SC.ShortProjectName);
|
|
try
|
|
{
|
|
ObbFile.Save();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private bool UpdateObbStoreVersion(string Filename)
|
|
{
|
|
string Version = Path.GetFileNameWithoutExtension(Filename).Split('.')[1];
|
|
|
|
using (ZipFile ObbFile = ZipFile.Read(Filename))
|
|
{
|
|
// Add the store version from the filename as a comment
|
|
ObbFile.Comment = String.Format("{0,10}", Version);
|
|
try
|
|
{
|
|
ObbFile.Save();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
private class OverflowFileInfo
|
|
{
|
|
public OverflowFileInfo(List<FileReference> InFilesForOverflow, Int64 InOverflowObbSize)
|
|
{
|
|
FilesForOverflow = InFilesForOverflow;
|
|
OverflowObbSize = InOverflowObbSize;
|
|
}
|
|
|
|
public List<FileReference> FilesForOverflow { get; set; }
|
|
public Int64 OverflowObbSize { get; set; }
|
|
}
|
|
|
|
public override void Package(ProjectParams Params, DeploymentContext SC, int WorkingCL)
|
|
{
|
|
if (SC.StageTargetConfigurations.Count != 1)
|
|
{
|
|
throw new AutomationException(ExitCode.Error_OnlyOneTargetConfigurationSupported, "Android is currently only able to package one target configuration at a time, but StageTargetConfigurations contained {0} configurations", SC.StageTargetConfigurations.Count);
|
|
}
|
|
|
|
UnrealTargetConfiguration TargetConfiguration = SC.StageTargetConfigurations[0];
|
|
|
|
UnrealArchitectures Architectures = GetDeploymentArchitectures(Params, SC);
|
|
bool bMakeSeparateApks = UnrealBuildTool.AndroidExports.ShouldMakeSeparateApks();
|
|
bool bBuildWithHiddenSymbolVisibility = BuildWithHiddenSymbolVisibility(SC);
|
|
bool bSaveSymbols = GetSaveSymbols(SC);
|
|
bool bEnableBundle = GetEnableBundle(SC);
|
|
bool bEnableUniversalAPK = GetEnableUniversalAPK(SC);
|
|
|
|
var Deploy = AndroidExports.CreateDeploymentHandler(Params.RawProjectPath, Params.ForcePackageData);
|
|
bool bPackageDataInsideApk = Deploy.GetPackageDataInsideApk();
|
|
|
|
bool bUseAFS = false;
|
|
bool bUseAFSProject = false;
|
|
|
|
bool bAFSEnablePlugin;
|
|
string AFSToken;
|
|
bool bIsShipping;
|
|
bool bAFSIncludeInShipping;
|
|
bool bAFSAllowExternalStartInShipping;
|
|
UsingAndroidFileServer(Params, SC, out bAFSEnablePlugin, out AFSToken, out bIsShipping, out bAFSIncludeInShipping, out bAFSAllowExternalStartInShipping);
|
|
|
|
if (bAFSEnablePlugin && !bPackageDataInsideApk)
|
|
{
|
|
bUseAFS = true;
|
|
// AFSProject APK should be used if shipping and AFS wasn't included
|
|
if (bIsShipping && !(bAFSIncludeInShipping && bAFSAllowExternalStartInShipping))
|
|
{
|
|
bUseAFSProject = true;
|
|
}
|
|
}
|
|
|
|
string BaseApkName = GetFinalApkName(Params, SC.StageExecutables[0], true, Architecture:null);
|
|
Logger.LogInformation("BaseApkName = {BaseApkName}", BaseApkName);
|
|
|
|
// Create main OBB with entire contents of staging dir. This
|
|
// includes any PAK files, movie files, etc.
|
|
|
|
string LocalObbName = SC.StageDirectory.FullName+".obb";
|
|
string LocalPatchName = SC.StageDirectory.FullName + ".patch.obb";
|
|
string LocalOverflowNameTemplate = SC.StageDirectory.FullName + ".overflow{0}.obb";
|
|
|
|
FileFilter ObbFileFilter = new FileFilter(FileFilterType.Include);
|
|
ConfigHierarchy EngineIni = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(Params.RawProjectPath), UnrealTargetPlatform.Android);
|
|
List<string> ObbFilters;
|
|
EngineIni.GetArray("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "ObbFilters", out ObbFilters);
|
|
if (ObbFilters != null)
|
|
{
|
|
ObbFileFilter.AddRules(ObbFilters);
|
|
}
|
|
// Filter out dynamic libraries from obb
|
|
ObbFileFilter.Exclude("*.so");
|
|
|
|
List<FileReference> FilesForObb = new List<FileReference>();
|
|
// Add staged Engine files
|
|
{
|
|
string EngineStageDirectoryPath = Path.Combine(SC.StageDirectory.FullName, "Engine");
|
|
FilesForObb.AddRange(ObbFileFilter.ApplyToDirectory(new DirectoryReference(EngineStageDirectoryPath), true));
|
|
}
|
|
// Add staged project files
|
|
{
|
|
string ProjectStageDirectoryPath = Path.Combine(SC.StageDirectory.FullName, SC.ShortProjectName);
|
|
FilesForObb.AddRange(ObbFileFilter.ApplyToDirectory(new DirectoryReference(ProjectStageDirectoryPath), true));
|
|
}
|
|
|
|
bool OBBNeedsUpdate = false;
|
|
|
|
if (File.Exists(LocalObbName))
|
|
{
|
|
System.DateTime OBBTimeStamp = File.GetLastWriteTimeUtc(LocalObbName);
|
|
foreach (FileReference FileToObb in FilesForObb)
|
|
{
|
|
System.DateTime FileTimeStamp = File.GetLastWriteTimeUtc(FileToObb.FullName);
|
|
if (FileTimeStamp > OBBTimeStamp)
|
|
{
|
|
OBBNeedsUpdate = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OBBNeedsUpdate = true;
|
|
}
|
|
Int64 OBBSizeAllowed = GetMaxOBBSizeAllowed(SC);
|
|
string LimitString = (OBBSizeAllowed < NormalOBBSizeAllowed) ? "1 GiB" : ((OBBSizeAllowed < MaxOBBSizeAllowed) ? "2 GiB" : "4 GiB");
|
|
|
|
if (!OBBNeedsUpdate)
|
|
{
|
|
Logger.LogInformation("{Text}", "OBB is up to date: " + LocalObbName);
|
|
}
|
|
else
|
|
{
|
|
// Always delete the target OBB file if it exists
|
|
if (File.Exists(LocalObbName))
|
|
{
|
|
File.Delete(LocalObbName);
|
|
}
|
|
|
|
// Always delete the target patch OBB file if it exists
|
|
if (File.Exists(LocalPatchName))
|
|
{
|
|
File.Delete(LocalPatchName);
|
|
}
|
|
|
|
// Always delete all target overflow OBB files if they exists
|
|
int OverflowIndex = 1;
|
|
while (File.Exists(string.Format(LocalOverflowNameTemplate, OverflowIndex)))
|
|
{
|
|
File.Delete(string.Format(LocalOverflowNameTemplate, OverflowIndex));
|
|
// move to next overflow
|
|
OverflowIndex++;
|
|
}
|
|
|
|
List<FileReference> FilesToObb = FilesForObb;
|
|
List<FileReference> FilesToPatch = new List<FileReference>();
|
|
List<OverflowFileInfo> OverflowInfos = new List<OverflowFileInfo>();
|
|
|
|
if (AllowPatchOBBFile(SC))
|
|
{
|
|
int AllowOverflowOBBLimit = AllowOverflowOBBFiles(SC);
|
|
|
|
FilesToObb = new List<FileReference>();
|
|
|
|
// Collect the filesize and place into Obb or Patch list
|
|
Int64 StagingDirLength = SC.StageDirectory.FullName.Length;
|
|
Int64 MinimumObbSize = 22 + 10; // EOCD with comment (store version)
|
|
Int64 MainObbSize = MinimumObbSize;
|
|
Int64 PatchObbSize = MinimumObbSize;
|
|
|
|
foreach (FileReference FileRef in FilesForObb)
|
|
{
|
|
FileInfo LocalFileInfo = new FileInfo(FileRef.FullName);
|
|
Int64 LocalFileLength = LocalFileInfo.Length;
|
|
Int64 FilenameLength = FileRef.FullName.Length - StagingDirLength - 1;
|
|
|
|
Int64 LocalOverhead = (30 + FilenameLength + 36); // local file descriptor
|
|
Int64 GlobalOverhead = (46 + FilenameLength + 36); // central directory cost
|
|
Int64 FileRequirements = LocalFileLength + LocalOverhead + GlobalOverhead;
|
|
|
|
if (MainObbSize + FileRequirements < OBBSizeAllowed)
|
|
{
|
|
FilesToObb.Add(FileRef);
|
|
MainObbSize += FileRequirements;
|
|
}
|
|
else if (PatchObbSize + FileRequirements < OBBSizeAllowed)
|
|
{
|
|
FilesToPatch.Add(FileRef);
|
|
PatchObbSize += FileRequirements;
|
|
}
|
|
else if (AllowOverflowOBBLimit > 0)
|
|
{
|
|
// find an overflow we can fit this file
|
|
bool bFoundOverflow = false;
|
|
foreach (OverflowFileInfo OverflowRef in OverflowInfos)
|
|
{
|
|
if (OverflowRef.OverflowObbSize + FileRequirements < OBBSizeAllowed)
|
|
{
|
|
OverflowRef.FilesForOverflow.Add(FileRef);
|
|
OverflowRef.OverflowObbSize += FileRequirements;
|
|
bFoundOverflow = true;
|
|
}
|
|
}
|
|
|
|
if (!bFoundOverflow)
|
|
{
|
|
if (AllowOverflowOBBLimit >= OverflowInfos.Count)
|
|
{
|
|
// create a new overflow obb
|
|
if (MinimumObbSize + FileRequirements < OBBSizeAllowed)
|
|
{
|
|
List<FileReference> NewObb = new List<FileReference>();
|
|
NewObb.Add(FileRef);
|
|
OverflowInfos.Add(new OverflowFileInfo(NewObb, MinimumObbSize + FileRequirements));
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("{Text}", "Failed to add " + FileRef.FullName + " to a new overflow as it is bigger than the allowed OBB size ");
|
|
throw new AutomationException(ExitCode.Error_AndroidOBBError, "Stage Failed. Could not add {1} ({0} bytes) to an OBB as OBBs are limited to {2} bytes.", FileRequirements, FileRef.FullName, OBBSizeAllowed);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("{Text}", "Failed to add required overflow OBB: " + LocalObbName);
|
|
throw new AutomationException(ExitCode.Error_AndroidOBBError, "Stage Failed. Overflow OBBs limited to a count of {0}. Contents are to big to fit.", OverflowInfos.Count);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// no room in either file and no overflows allowed
|
|
Logger.LogInformation("{Text}", "Failed to build OBB: " + LocalObbName);
|
|
throw new AutomationException(ExitCode.Error_AndroidOBBError, "Stage Failed. Could not build OBB {0}. The file may be too big to fit in an OBB ({1} limit and no overflows are permitted)", LocalObbName, LimitString);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now create the main OBB as a ZIP archive.
|
|
if (!CreateOBBFile(SC, LocalObbName, FilesToObb))
|
|
{
|
|
Logger.LogInformation("{Text}", "Failed to build OBB: " + LocalObbName);
|
|
throw new AutomationException(ExitCode.Error_AndroidOBBError, "Stage Failed. Could not build OBB {0}. The file may be too big to fit in an OBB ({1} limit)", LocalObbName, LimitString);
|
|
}
|
|
|
|
// Now create the patch OBB as a ZIP archive if required.
|
|
if (FilesToPatch.Count() > 0)
|
|
{
|
|
if (!CreateOBBFile(SC, LocalPatchName, FilesToPatch))
|
|
{
|
|
Logger.LogInformation("{Text}", "Failed to build OBB: " + LocalPatchName);
|
|
throw new AutomationException(ExitCode.Error_AndroidOBBError, "Stage Failed. Could not build OBB {0}. The file may be too big to fit in an OBB ({1} limit)", LocalPatchName, LimitString);
|
|
}
|
|
}
|
|
|
|
OverflowIndex = 1;
|
|
foreach (OverflowFileInfo OverflowRef in OverflowInfos)
|
|
{
|
|
if (OverflowRef.FilesForOverflow.Count() > 0)
|
|
{
|
|
string LocalOverflowName = string.Format(LocalOverflowNameTemplate, OverflowIndex);
|
|
if (!CreateOBBFile(SC, LocalOverflowName, OverflowRef.FilesForOverflow))
|
|
{
|
|
Logger.LogInformation("{Text}", "Failed to build OBB: " + LocalOverflowName);
|
|
throw new AutomationException(ExitCode.Error_AndroidOBBError, "Stage Failed. Could not build OBB {0}. The file may be too big to fit in an OBB ({1} limit)", LocalOverflowName, LimitString);
|
|
}
|
|
}
|
|
OverflowIndex++;
|
|
}
|
|
}
|
|
|
|
// make sure the OBB is <= 2GiB (or 4GiB if large OBB enabled)
|
|
FileInfo OBBFileInfo = new FileInfo(LocalObbName);
|
|
Int64 ObbFileLength = OBBFileInfo.Length;
|
|
if (ObbFileLength > OBBSizeAllowed)
|
|
{
|
|
Logger.LogInformation("{Text}", "OBB exceeds " + LimitString + " limit: " + ObbFileLength + " bytes");
|
|
throw new AutomationException(ExitCode.Error_AndroidOBBError, "Stage Failed. OBB {0} exceeds {1} limit)", LocalObbName, LimitString);
|
|
}
|
|
|
|
// collect plugin extra data paths from target receipts
|
|
Deploy.SetAndroidPluginData(Architectures, CollectPluginDataPaths(SC));
|
|
|
|
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(Params.RawProjectPath), UnrealTargetPlatform.Android);
|
|
int MinSDKVersion;
|
|
Ini.GetInt32("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "MinSDKVersion", out MinSDKVersion);
|
|
int TargetSDKVersion = MinSDKVersion;
|
|
Ini.GetInt32("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "TargetSDKVersion", out TargetSDKVersion);
|
|
Logger.LogInformation("{Text}", "Target SDK Version " + TargetSDKVersion);
|
|
bool bDisablePerfHarden = false;
|
|
if (TargetConfiguration != UnrealTargetConfiguration.Shipping)
|
|
{
|
|
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bEnableMaliPerfCounters", out bDisablePerfHarden);
|
|
}
|
|
|
|
foreach (UnrealArch Architecture in Architectures.Architectures)
|
|
{
|
|
string ApkName = GetFinalApkName(Params, SC.StageExecutables[0], true, bMakeSeparateApks ? Architecture : null);
|
|
string ApkBareName = GetFinalApkName(Params, SC.StageExecutables[0], true, null);
|
|
bool bHaveAPK = !bEnableBundle; // do not have a standard APK if bundle enabled
|
|
if (!SC.IsCodeBasedProject)
|
|
{
|
|
string UnrealSOName = GetFinalApkName(Params, SC.StageExecutables[0], false, bMakeSeparateApks ? Architecture : null);
|
|
UnrealSOName = UnrealSOName.Replace(".apk", ".so");
|
|
if (FileExists_NoExceptions(UnrealSOName) == false)
|
|
{
|
|
Logger.LogInformation("{Text}", "Failed to find game .so " + UnrealSOName);
|
|
throw new AutomationException(ExitCode.Error_MissingExecutable, "Stage Failed. Could not find .so {0}. You may need to build the UE project with your target configuration and platform.", UnrealSOName);
|
|
}
|
|
}
|
|
|
|
TargetReceipt Receipt = SC.StageTargets[0].Receipt;
|
|
|
|
// when we make an embedded executable, all we do is output to libUnreal.so - we don't need to make an APK at all
|
|
// however, we still let package go through to make the .obb file
|
|
string CookFlavor = SC.FinalCookPlatform.IndexOf("_") > 0 ? SC.FinalCookPlatform.Substring(SC.FinalCookPlatform.IndexOf("_")) : "";
|
|
if (!Params.Prebuilt)
|
|
{
|
|
string SOName = GetSONameWithoutArchitecture(Params, SC.StageExecutables[0]);
|
|
bool bShouldCompileAsDll = Receipt.HasValueForAdditionalProperty("CompileAsDll", "true");
|
|
if (bShouldCompileAsDll)
|
|
{
|
|
// MakeApk
|
|
SOName = Receipt.BuildProducts[0].Path.FullName;
|
|
|
|
// saving package info, which will allow
|
|
TargetType Type = TargetType.Game;
|
|
if (CookFlavor.EndsWith("Client"))
|
|
{
|
|
Type = TargetType.Client;
|
|
}
|
|
else if (CookFlavor.EndsWith("Server"))
|
|
{
|
|
Type = TargetType.Server;
|
|
}
|
|
Logger.LogInformation("SavePackageInfo");
|
|
Deploy.SavePackageInfo(Params.ShortProjectName, SC.ProjectRoot.FullName, Type, true);
|
|
}
|
|
Deploy.PrepForUATPackageOrDeploy(Params.RawProjectPath, Params.ShortProjectName, SC.ProjectRoot, SOName, SC.LocalRoot + "/Engine", Params.Distribution, CookFlavor, SC.StageTargets[0].Receipt.Configuration, false, bShouldCompileAsDll, SC.Archive);
|
|
}
|
|
|
|
// Create APK specific OBB in case we have a detached OBB.
|
|
string DeviceObbName = "";
|
|
string ObbName = "";
|
|
string DevicePatchName = "";
|
|
string PatchName = "";
|
|
|
|
List<OverflowBatchInstallInfo> OverflowInfos = new List<OverflowBatchInstallInfo>();
|
|
|
|
if (!bPackageDataInsideApk)
|
|
{
|
|
DeviceObbName = GetDeviceObbName(ApkName, SC);
|
|
ObbName = GetFinalObbName(ApkName, SC);
|
|
CopyFile(LocalObbName, ObbName);
|
|
|
|
// apply store version to OBB to make it unique for PlayStore upload
|
|
UpdateObbStoreVersion(ObbName);
|
|
|
|
if (File.Exists(LocalPatchName))
|
|
{
|
|
DevicePatchName = GetDevicePatchName(ApkName, SC);
|
|
PatchName = GetFinalPatchName(ApkName, SC);
|
|
CopyFile(LocalPatchName, PatchName);
|
|
|
|
// apply store version to OBB to make it unique for PlayStore upload
|
|
UpdateObbStoreVersion(PatchName);
|
|
}
|
|
|
|
int OverflowIndex = 1;
|
|
while (File.Exists(string.Format(LocalOverflowNameTemplate, OverflowIndex)))
|
|
{
|
|
string DeviceOverflowName = GetDeviceOverflowName(ApkName, SC, OverflowIndex);
|
|
string OverflowName = GetFinalOverflowName(ApkName, SC, OverflowIndex);
|
|
|
|
OverflowInfos.Add(new OverflowBatchInstallInfo(OverflowIndex, DeviceOverflowName, OverflowName, false));
|
|
CopyFile(string.Format(LocalOverflowNameTemplate, OverflowIndex), OverflowName);
|
|
|
|
// apply store version to OBB to make it unique for PlayStore upload
|
|
UpdateObbStoreVersion(OverflowName);
|
|
|
|
OverflowIndex++;
|
|
}
|
|
}
|
|
|
|
// check for optional universal apk
|
|
string APKDirectory = Path.GetDirectoryName(ApkName);
|
|
string APKNameWithoutExtension = Path.GetFileNameWithoutExtension(ApkName);
|
|
string APKBareNameWithoutExtension = Path.GetFileNameWithoutExtension(ApkBareName);
|
|
string UniversalApkName = Path.Combine(APKDirectory, APKNameWithoutExtension + "_universal.apk");
|
|
bool bHaveUniversal = false;
|
|
if (bEnableBundle && bEnableUniversalAPK)
|
|
{
|
|
if (FileExists(UniversalApkName))
|
|
{
|
|
bHaveUniversal = true;
|
|
}
|
|
else
|
|
{
|
|
UniversalApkName = Path.Combine(APKDirectory, APKBareNameWithoutExtension + "_universal.apk");
|
|
if (FileExists(UniversalApkName))
|
|
{
|
|
bHaveUniversal = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
//figure out which platforms we need to create install files for
|
|
bool bNeedsPCInstall = false;
|
|
bool bNeedsMacInstall = false;
|
|
bool bNeedsLinuxInstall = false;
|
|
GetPlatformInstallOptions(SC, out bNeedsPCInstall, out bNeedsMacInstall, out bNeedsLinuxInstall);
|
|
|
|
//helper delegate to prevent code duplication but allow us access to all the local variables we need
|
|
var CreateInstallFilesAction = new Action<UnrealTargetPlatform>(Target =>
|
|
{
|
|
bool bIsPC = (Target == UnrealTargetPlatform.Win64);
|
|
string LineEnding = bIsPC ? "\r\n" : "\n";
|
|
// Write install batch file(s).
|
|
string PackageName = GetPackageInfo(ApkName, SC, false);
|
|
string BatchName = GetFinalBatchName(ApkName, SC, false, EBatchType.Install, Target);
|
|
List<string> InstallBatchLines = GenerateInstallBatchFile(bPackageDataInsideApk, PackageName, ApkName, Params, ObbName, DeviceObbName, false, PatchName, DevicePatchName, false, OverflowInfos,
|
|
bIsPC, Params.Distribution, TargetSDKVersion > 22, bDisablePerfHarden, bUseAFS, bUseAFSProject, AFSToken, Target);
|
|
if (bHaveAPK)
|
|
{
|
|
// make a batch file that can be used to install the .apk and .obb files
|
|
File.WriteAllText(BatchName, string.Join(LineEnding, InstallBatchLines) + LineEnding);
|
|
}
|
|
// make a batch file that can be used to uninstall the .apk and .obb files
|
|
string UninstallBatchName = GetFinalBatchName(ApkName, SC, false, EBatchType.Uninstall, Target);
|
|
string[] UninstallBatchLines = GenerateUninstallBatchFile(bPackageDataInsideApk, PackageName, ApkName, Params, bIsPC);
|
|
if (bHaveAPK || bHaveUniversal)
|
|
{
|
|
File.WriteAllText(UninstallBatchName, string.Join(LineEnding, UninstallBatchLines) + LineEnding);
|
|
}
|
|
|
|
string UniversalBatchName = "";
|
|
if (bHaveUniversal)
|
|
{
|
|
UniversalBatchName = GetFinalBatchName(UniversalApkName, SC, false, EBatchType.Install, Target);
|
|
// make a batch file that can be used to install the .apk
|
|
List<string> UniversalBatchLines = GenerateInstallBatchFile(bPackageDataInsideApk, PackageName, UniversalApkName, Params, ObbName, DeviceObbName, false, PatchName, DevicePatchName, false, OverflowInfos,
|
|
bIsPC, Params.Distribution, TargetSDKVersion > 22, bDisablePerfHarden, bUseAFS, bUseAFSProject, AFSToken, Target);
|
|
File.WriteAllText(UniversalBatchName, string.Join(LineEnding, UniversalBatchLines) + LineEnding);
|
|
}
|
|
|
|
string SymbolizeBatchName = GetFinalBatchName(ApkName, SC, false, EBatchType.Symbolize, Target);
|
|
// Deprecated check of bBuildWithHiddenSymbolVisibility. Remove in 5.8+
|
|
if (bBuildWithHiddenSymbolVisibility || bSaveSymbols)
|
|
{
|
|
string[] UniversalSymbolizeBatchLines = GenerateSymbolizeBatchFile(Params, PackageName, ApkName, SC, Architecture, bIsPC);
|
|
File.WriteAllText(SymbolizeBatchName, string.Join(LineEnding, UniversalSymbolizeBatchLines) + LineEnding);
|
|
}
|
|
|
|
if (!OperatingSystem.IsWindows())
|
|
{
|
|
if (bHaveAPK)
|
|
{
|
|
CommandUtils.FixUnixFilePermissions(BatchName);
|
|
}
|
|
if (bHaveAPK || bHaveUniversal)
|
|
{
|
|
CommandUtils.FixUnixFilePermissions(UninstallBatchName);
|
|
}
|
|
if (bHaveUniversal)
|
|
{
|
|
CommandUtils.FixUnixFilePermissions(UniversalBatchName);
|
|
}
|
|
// Deprecated check of bBuildWithHiddenSymbolVisibility. Remove in 5.8+
|
|
if (bBuildWithHiddenSymbolVisibility || bSaveSymbols)
|
|
{
|
|
CommandUtils.FixUnixFilePermissions(SymbolizeBatchName);
|
|
}
|
|
//if(File.Exists(NoInstallBatchName))
|
|
//{
|
|
// CommandUtils.FixUnixFilePermissions(NoInstallBatchName);
|
|
//}
|
|
}
|
|
});
|
|
|
|
if (bNeedsPCInstall)
|
|
{
|
|
CreateInstallFilesAction.Invoke(UnrealTargetPlatform.Win64);
|
|
}
|
|
if (bNeedsMacInstall)
|
|
{
|
|
CreateInstallFilesAction.Invoke(UnrealTargetPlatform.Mac);
|
|
}
|
|
if (bNeedsLinuxInstall)
|
|
{
|
|
CreateInstallFilesAction.Invoke(UnrealTargetPlatform.Linux);
|
|
}
|
|
|
|
// If we aren't packaging data in the APK then lets write out a bat file to also let us test without the OBB
|
|
// on the device.
|
|
//String NoInstallBatchName = GetFinalBatchName(ApkName, Params, bMakeSeparateApks ? Architecture : "", bMakeSeparateApks ? GPUArchitecture : "", true, false);
|
|
// if(!bPackageDataInsideApk)
|
|
//{
|
|
// BatchLines = GenerateInstallBatchFile(bPackageDataInsideApk, PackageName, ApkName, Params, ObbName, DeviceObbName, true);
|
|
// File.WriteAllLines(NoInstallBatchName, BatchLines);
|
|
//}
|
|
}
|
|
|
|
PrintRunTime();
|
|
}
|
|
|
|
string GetAFSExecutable(UnrealTargetPlatform Target)
|
|
{
|
|
return AndroidExports.GetAFSExecutable(Target, Logger);
|
|
}
|
|
|
|
private class OverflowBatchInstallInfo
|
|
{
|
|
public OverflowBatchInstallInfo(int InOverflowIndex, string InDeviceOverflowName, string InOverflowName, bool InNoOverflowInstall)
|
|
{
|
|
OverflowIndex = InOverflowIndex;
|
|
DeviceOverflowName = InDeviceOverflowName;
|
|
OverflowName = InOverflowName;
|
|
bNoOverflowInstall = InNoOverflowInstall;
|
|
}
|
|
|
|
public int OverflowIndex { get; }
|
|
public string DeviceOverflowName { get; }
|
|
public string OverflowName { get; }
|
|
public bool bNoOverflowInstall { get; }
|
|
}
|
|
|
|
private List<string> GenerateInstallBatchFile(bool bPackageDataInsideApk, string PackageName, string ApkName, ProjectParams Params, string ObbName, string DeviceObbName, bool bNoObbInstall,
|
|
string PatchName, string DevicePatchName, bool bNoPatchInstall, List<OverflowBatchInstallInfo> OverflowInfo,
|
|
bool bIsPC, bool bIsDistribution, bool bRequireRuntimeStoragePermission, bool bDisablePerfHarden, bool bUseAFS, bool bUseAFSProject, string AFSToken, UnrealTargetPlatform Target)
|
|
{
|
|
List<string> BatchLines = new List<string>();
|
|
string ReadPermissionGrantCommand = "shell pm grant " + PackageName + " android.permission.READ_EXTERNAL_STORAGE";
|
|
string WritePermissionGrantCommand = "shell pm grant " + PackageName + " android.permission.WRITE_EXTERNAL_STORAGE";
|
|
string ForegroundPermissionGrantCommand = "shell pm grant " + PackageName + " android.permission.FOREGROUND_SERVICE";
|
|
string ForegroundDataSyncPermissionGrantCommand = "shell pm grant " + PackageName + " android.permission.FOREGROUND_SERVICE_DATA_SYNC";
|
|
string NotificationPermissionGrantCommand = "shell pm grant " + PackageName + " android.permission.POST_NOTIFICATIONS";
|
|
string DisablePerfHardenCommand = "shell setprop security.perf_harden 0";
|
|
|
|
string NullCmd = bIsPC ? " >nul 2>&1" : " >/dev/null 2>&1";
|
|
|
|
// We don't grant runtime permission for distribution build on purpose since we will push the obb file to the folder that doesn't require runtime storage permission.
|
|
// This way developer can catch permission issue if they try to save/load game file in folder that requires runtime storage permission.
|
|
bool bNeedGrantStoragePermission = bRequireRuntimeStoragePermission && !bIsDistribution;
|
|
bool bNeedGrantForegroundPermission = bUseAFS || bUseAFSProject;
|
|
|
|
// We can't always push directly to Android/obb so uploads to Download then moves it
|
|
bool bDontMoveOBB = bUseAFS ? true : bPackageDataInsideApk;
|
|
|
|
bool bHavePatch = (PatchName != "");
|
|
|
|
string AFSExecutable = GetAFSExecutable(Target);
|
|
string AFSCommonArg = "-p " + PackageName;
|
|
if (AFSToken != "")
|
|
{
|
|
AFSCommonArg += " -k " + AFSToken;
|
|
}
|
|
|
|
if (!bIsPC)
|
|
{
|
|
string APKInstallCommand = "$ADB $DEVICE install " + Path.GetFileName(ApkName);
|
|
string APKReinstallCommand = "";
|
|
|
|
// If it is a distribution build, push to $STORAGE/Android/obb folder instead of $STORAGE/obb folder.
|
|
// Note that $STORAGE/Android/obb will be the folder that contains the obb if you download the app from playstore.
|
|
string OBBInstallCommand = bNoObbInstall ? "\t$ADB $DEVICE shell 'rm -r $EXTERNAL_STORAGE/" + DeviceObbName + "'" : "\t$ADB $DEVICE push " + Path.GetFileName(ObbName) + " " + TargetAndroidTemp + DeviceObbName;
|
|
string PatchInstallCommand = bNoPatchInstall ? "\t$ADB $DEVICE shell 'rm -r $EXTERNAL_STORAGE/" + DevicePatchName + "'" : "\t$ADB $DEVICE push " + Path.GetFileName(PatchName) + TargetAndroidTemp + DevicePatchName;
|
|
|
|
List<string> OverflowInstallCommands = new List<string>();
|
|
|
|
if (bUseAFS)
|
|
{
|
|
if (bUseAFSProject)
|
|
{
|
|
APKInstallCommand = "$ADB $DEVICE install AFS_" + Path.GetFileName(ApkName);
|
|
APKReinstallCommand = "$ADB $DEVICE install -r " + Path.GetFileName(ApkName);
|
|
}
|
|
else
|
|
{
|
|
// stop the fileserver (not needed on reinstall above
|
|
APKReinstallCommand = "$AFS $DEVICE " + AFSCommonArg + " stop-all";
|
|
}
|
|
string AFSCommand = "\t$AFS $DEVICE " + AFSCommonArg;
|
|
OBBInstallCommand = bNoObbInstall ? AFSCommand + " deletefile '^mainobb'" : AFSCommand + " push " + Path.GetFileName(ObbName) + " '^mainobb'";
|
|
PatchInstallCommand = bNoPatchInstall ? AFSCommand + " deletefile '^patchobb'" : AFSCommand + " push " + Path.GetFileName(PatchName) + " '^patchobb'";
|
|
if (!bPackageDataInsideApk)
|
|
{
|
|
foreach (OverflowBatchInstallInfo Overflow in OverflowInfo)
|
|
{
|
|
string AFSOverflowName = string.Format("^overflow{0}obb", Overflow.OverflowIndex);
|
|
OverflowInstallCommands.Add(Overflow.bNoOverflowInstall ? AFSCommand + " deletefile '" + AFSOverflowName + "'" : AFSCommand + " push " + Path.GetFileName(Overflow.OverflowName) + " '" + AFSOverflowName + "'");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!bPackageDataInsideApk)
|
|
{
|
|
foreach (OverflowBatchInstallInfo Overflow in OverflowInfo)
|
|
{
|
|
OverflowInstallCommands.Add(Overflow.bNoOverflowInstall ? "\t$ADB $DEVICE shell 'rm -r $EXTERNAL_STORAGE/" + Overflow.DeviceOverflowName + "'" : "\t$ADB $DEVICE push " + Path.GetFileName(Overflow.OverflowName) + " " + TargetAndroidTemp + Overflow.DeviceOverflowName);
|
|
}
|
|
}
|
|
}
|
|
|
|
Logger.LogInformation("Writing shell script for install with {Arg0}", bPackageDataInsideApk ? "data in APK" : "separate obb");
|
|
BatchLines.AddRange(new string[] {
|
|
"#!/bin/sh",
|
|
"cd \"`dirname \"$0\"`\"",
|
|
"AFS=./" + AFSExecutable,
|
|
"ADB=",
|
|
"if [ \"$ANDROID_HOME\" != \"\" ]; then ADB=$ANDROID_HOME/platform-tools/adb; else ADB=" +Environment.GetEnvironmentVariable("ANDROID_HOME") + "/platform-tools/adb; fi",
|
|
"DEVICE=",
|
|
"if [ \"$1\" != \"\" ]; then DEVICE=\"-s $1\"; fi",
|
|
"echo",
|
|
"echo Uninstalling existing application. Failures here can almost always be ignored.",
|
|
"$ADB $DEVICE uninstall " + PackageName,
|
|
"echo",
|
|
"echo Installing existing application. Failures here indicate a problem with the device \\(connection or storage permissions\\) and are fatal.",
|
|
APKInstallCommand,
|
|
"if [ $? -eq 0 ]; then",
|
|
"\techo",
|
|
"\t$ADB $DEVICE shell pm list packages " + PackageName,
|
|
bNeedGrantForegroundPermission ? "\t$ADB $DEVICE " + ForegroundPermissionGrantCommand + NullCmd : "",
|
|
bNeedGrantForegroundPermission ? "\t$ADB $DEVICE " + ForegroundDataSyncPermissionGrantCommand + NullCmd : "",
|
|
bNeedGrantForegroundPermission ? "\t$ADB $DEVICE " + NotificationPermissionGrantCommand + NullCmd : "",
|
|
"\techo",
|
|
"\techo Removing old data. Failures here are usually fine - indicating the files were not on the device.",
|
|
"\t$ADB $DEVICE shell 'rm -r $EXTERNAL_STORAGE/UnrealGame/" + Params.ShortProjectName + "'",
|
|
"\t$ADB $DEVICE shell 'rm -r $EXTERNAL_STORAGE/UnrealGame/UECommandLine.txt" + "'",
|
|
"\t$ADB $DEVICE shell 'rm -r $EXTERNAL_STORAGE/" + TargetAndroidLocation + PackageName + "'",
|
|
"\t$ADB $DEVICE shell 'rm -r $EXTERNAL_STORAGE/Android/" + TargetAndroidLocation + PackageName + "'",
|
|
"\t$ADB $DEVICE shell 'rm -r $EXTERNAL_STORAGE/Download/" + TargetAndroidLocation + PackageName + "'",
|
|
bPackageDataInsideApk ? "" : "\techo",
|
|
bPackageDataInsideApk ? "" : "\techo Installing new data. Failures here indicate storage problems \\(missing SD card or bad permissions\\) and are fatal.",
|
|
bPackageDataInsideApk ? "" : "\tSTORAGE=$(echo \"`$ADB $DEVICE shell 'echo $EXTERNAL_STORAGE'`\" | cat -v | tr -d '^M')",
|
|
bPackageDataInsideApk ? "" : OBBInstallCommand,
|
|
bPackageDataInsideApk ? "if [ 1 ]; then" : "\tif [ $? -eq 0 ]; then",
|
|
!bHavePatch ? "" : (bPackageDataInsideApk ? "" : PatchInstallCommand)});
|
|
|
|
BatchLines.AddRange(OverflowInstallCommands);
|
|
|
|
BatchLines.AddRange(new string[] {
|
|
bDontMoveOBB ? "" : "\t\t$ADB $DEVICE shell mkdir $STORAGE/Android/" + TargetAndroidLocation + PackageName, // don't check for error since installing may create the obb directory
|
|
bDontMoveOBB ? "" : "\t\t$ADB $DEVICE shell mv " + TargetAndroidTemp + TargetAndroidLocation + PackageName + " $STORAGE/Android/" + TargetAndroidLocation,
|
|
bDontMoveOBB ? "" : "\t\t$ADB $DEVICE shell rm -r " + TargetAndroidTemp + TargetAndroidLocation,
|
|
APKReinstallCommand,
|
|
bNeedGrantStoragePermission ? "\techo Grant READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE to the apk for reading OBB or game file in external storage." : "",
|
|
bNeedGrantStoragePermission ? "\t$ADB $DEVICE " + ReadPermissionGrantCommand + NullCmd : "",
|
|
bNeedGrantStoragePermission ? "\t$ADB $DEVICE " + WritePermissionGrantCommand + NullCmd : "",
|
|
bDisablePerfHarden ? "\t$ADB $DEVICE " + DisablePerfHardenCommand : "",
|
|
"\t\techo",
|
|
"\t\techo Installation successful",
|
|
"\t\texit 0",
|
|
"\tfi",
|
|
"fi",
|
|
"echo",
|
|
"echo There was an error installing the game or the obb file. Look above for more info.",
|
|
"echo",
|
|
"echo Things to try:",
|
|
"echo 'Check that the device (and only the device) is listed with \\\"$ADB devices\\\" from a command prompt.'",
|
|
"echo Make sure all Developer options look normal on the device",
|
|
"echo Check that the device has an SD card.",
|
|
"exit 1"
|
|
});
|
|
}
|
|
else
|
|
{
|
|
string APKInstallCommand = "%ADB% %DEVICE% install " + Path.GetFileName(ApkName);
|
|
string APKReinstallCommand = "";
|
|
|
|
string OBBInstallCommand = bNoObbInstall ? "%ADB% %DEVICE% shell rm -r %STORAGE%/" + DeviceObbName : "%ADB% %DEVICE% push " + Path.GetFileName(ObbName) + " " + TargetAndroidTemp + DeviceObbName;
|
|
string PatchInstallCommand = bNoPatchInstall ? "%ADB% %DEVICE% shell rm -r %STORAGE%/" + DevicePatchName : "%ADB% %DEVICE% push " + Path.GetFileName(PatchName) + " " + TargetAndroidTemp + DevicePatchName;
|
|
|
|
List<string> OverflowInstallCommands = new List<string>();
|
|
|
|
if (bUseAFS)
|
|
{
|
|
if (bUseAFSProject)
|
|
{
|
|
APKInstallCommand = "%ADB% %DEVICE% install AFS_" + Path.GetFileName(ApkName);
|
|
APKReinstallCommand = "%ADB% %DEVICE% install -r " + Path.GetFileName(ApkName);
|
|
}
|
|
else
|
|
{
|
|
// stop the fileserver (not needed on reinstall above
|
|
APKReinstallCommand = "%AFS% %DEVICE% " + AFSCommonArg + " stop-all";
|
|
}
|
|
string AFSCommand = "\t%AFS% %DEVICE% " + AFSCommonArg;
|
|
OBBInstallCommand = bNoObbInstall ? AFSCommand + " deletefile \"^mainobb\"" : AFSCommand + " push " + Path.GetFileName(ObbName) + " \"^mainobb\"";
|
|
PatchInstallCommand = bNoPatchInstall ? AFSCommand + " deletefile \"^patchobb\"" : AFSCommand + " push " + Path.GetFileName(PatchName) + " \"^patchobb\"";
|
|
|
|
if (!bPackageDataInsideApk)
|
|
{
|
|
foreach (OverflowBatchInstallInfo Overflow in OverflowInfo)
|
|
{
|
|
string AFSOverflowName = string.Format("^overflow{0}obb", Overflow.OverflowIndex);
|
|
OverflowInstallCommands.Add(Overflow.bNoOverflowInstall ? AFSCommand + " deletefile \"" + AFSOverflowName + "\"" : AFSCommand + " push " + Path.GetFileName(Overflow.OverflowName) + " \"" + AFSOverflowName + "\"");
|
|
OverflowInstallCommands.Add("if \"%ERRORLEVEL%\" NEQ \"0\" goto Error");
|
|
}
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
if (!bPackageDataInsideApk)
|
|
{
|
|
foreach (OverflowBatchInstallInfo Overflow in OverflowInfo)
|
|
{
|
|
OverflowInstallCommands.Add(Overflow.bNoOverflowInstall ? "%ADB% %DEVICE% shell rm -r %STORAGE%/" + Overflow.DeviceOverflowName : "%ADB% %DEVICE% push " + Path.GetFileName(Overflow.OverflowName) + " " + TargetAndroidTemp + Overflow.DeviceOverflowName);
|
|
OverflowInstallCommands.Add("if \"%ERRORLEVEL%\" NEQ \"0\" goto Error");
|
|
}
|
|
}
|
|
}
|
|
|
|
Logger.LogInformation("Writing bat for install with {Arg0}", bPackageDataInsideApk ? "data in APK" : "separate OBB");
|
|
BatchLines.AddRange(new string[] {
|
|
"setlocal",
|
|
"if NOT \"%UE_SDKS_ROOT%\"==\"\" (call %UE_SDKS_ROOT%\\HostWin64\\Android\\SetupEnvironmentVars.bat)",
|
|
"set ANDROIDHOME=%ANDROID_HOME%",
|
|
"if \"%ANDROIDHOME%\"==\"\" set ANDROIDHOME="+Environment.GetEnvironmentVariable("ANDROID_HOME"),
|
|
"set ADB=%ANDROIDHOME%\\platform-tools\\adb.exe",
|
|
"set AFS=.\\" + AFSExecutable.Replace("/", "\\"),
|
|
"set DEVICE=",
|
|
"if not \"%1\"==\"\" set DEVICE=-s %1",
|
|
"for /f \"delims=\" %%A in ('%ADB% %DEVICE% " + GetStorageQueryCommand(true) +"') do @set STORAGE=%%A",
|
|
"@echo.",
|
|
"@echo Uninstalling existing application. Failures here can almost always be ignored.",
|
|
"%ADB% %DEVICE% uninstall " + PackageName,
|
|
"@echo.",
|
|
"@echo Installing existing application. Failures here indicate a problem with the device (connection or storage permissions) and are fatal.",
|
|
APKInstallCommand,
|
|
"@if \"%ERRORLEVEL%\" NEQ \"0\" goto Error",
|
|
"%ADB% %DEVICE% shell pm list packages " + PackageName,
|
|
bNeedGrantForegroundPermission ? "%ADB% %DEVICE% " + ForegroundPermissionGrantCommand + NullCmd : "",
|
|
bNeedGrantForegroundPermission ? "%ADB% %DEVICE% " + ForegroundDataSyncPermissionGrantCommand + NullCmd : "",
|
|
bNeedGrantForegroundPermission ? "%ADB% %DEVICE% " + NotificationPermissionGrantCommand + NullCmd : "",
|
|
"%ADB% %DEVICE% shell rm -r %STORAGE%/UnrealGame/" + Params.ShortProjectName,
|
|
"%ADB% %DEVICE% shell rm -r %STORAGE%/UnrealGame/UECommandLine.txt", // we need to delete the commandline in UnrealGame or it will mess up loading
|
|
"%ADB% %DEVICE% shell rm -r %STORAGE%/" + TargetAndroidLocation + PackageName,
|
|
"%ADB% %DEVICE% shell rm -r %STORAGE%/Android/" + TargetAndroidLocation + PackageName,
|
|
"%ADB% %DEVICE% shell rm -r %STORAGE%/Download/" + TargetAndroidLocation + PackageName,
|
|
bPackageDataInsideApk ? "" : "@echo.",
|
|
bPackageDataInsideApk ? "" : "@echo Installing new data. Failures here indicate storage problems (missing SD card or bad permissions) and are fatal.",
|
|
bPackageDataInsideApk ? "" : OBBInstallCommand,
|
|
bPackageDataInsideApk ? "" : "if \"%ERRORLEVEL%\" NEQ \"0\" goto Error",
|
|
!bHavePatch ? "" : (bPackageDataInsideApk ? "" : PatchInstallCommand),
|
|
!bHavePatch ? "" : (bPackageDataInsideApk ? "" : "if \"%ERRORLEVEL%\" NEQ \"0\" goto Error")});
|
|
|
|
BatchLines.AddRange(OverflowInstallCommands);
|
|
|
|
BatchLines.AddRange(new string[] {
|
|
bDontMoveOBB ? "" : "%ADB% %DEVICE% shell mkdir %STORAGE%/Android/" + TargetAndroidLocation + PackageName, // don't check for error since installing may create the obb directory
|
|
bDontMoveOBB ? "" : "%ADB% %DEVICE% shell mv " + TargetAndroidTemp + TargetAndroidLocation + PackageName + " %STORAGE%/Android/" + TargetAndroidLocation,
|
|
bDontMoveOBB ? "" : "if \"%ERRORLEVEL%\" NEQ \"0\" goto Error",
|
|
bDontMoveOBB ? "" : "%ADB% %DEVICE% shell rm -r " + TargetAndroidTemp + TargetAndroidLocation,
|
|
APKReinstallCommand,
|
|
bUseAFSProject ? "if \"%ERRORLEVEL%\" NEQ \"0\" goto Error" : "",
|
|
"@echo.",
|
|
bNeedGrantStoragePermission ? "@echo Grant READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE to the apk for reading OBB file or game file in external storage." : "",
|
|
bNeedGrantStoragePermission ? "%ADB% %DEVICE% " + ReadPermissionGrantCommand + NullCmd : "",
|
|
bNeedGrantStoragePermission ? "%ADB% %DEVICE% " + WritePermissionGrantCommand + NullCmd : "",
|
|
bDisablePerfHarden ? "%ADB% %DEVICE% " + DisablePerfHardenCommand : "",
|
|
"@echo.",
|
|
"@echo Installation successful",
|
|
"goto:eof",
|
|
":Error",
|
|
"@echo.",
|
|
"@echo There was an error installing the game or the obb file. Look above for more info.",
|
|
"@echo.",
|
|
"@echo Things to try:",
|
|
"@echo Check that the device (and only the device) is listed with \"%ADB$ devices\" from a command prompt.",
|
|
"@echo Make sure all Developer options look normal on the device",
|
|
"@echo Check that the device has an SD card.",
|
|
"@pause"
|
|
});
|
|
}
|
|
return BatchLines;
|
|
}
|
|
|
|
|
|
private string[] GenerateUninstallBatchFile(bool bPackageDataInsideApk, string PackageName, string ApkName, ProjectParams Params, bool bIsPC)
|
|
{
|
|
string[] BatchLines = null;
|
|
|
|
if (!bIsPC)
|
|
{
|
|
Logger.LogInformation("Writing shell script for uninstall with {Arg0}", bPackageDataInsideApk ? "data in APK" : "separate obb");
|
|
BatchLines = new string[] {
|
|
"#!/bin/sh",
|
|
"cd \"`dirname \"$0\"`\"",
|
|
"ADB=",
|
|
"if [ \"$ANDROID_HOME\" != \"\" ]; then ADB=$ANDROID_HOME/platform-tools/adb; else ADB=" +Environment.GetEnvironmentVariable("ANDROID_HOME") + "/platform-tools/adb; fi",
|
|
"DEVICE=",
|
|
"if [ \"$1\" != \"\" ]; then DEVICE=\"-s $1\"; fi",
|
|
"echo",
|
|
"echo Uninstalling existing application. Failures here can almost always be ignored.",
|
|
"$ADB $DEVICE uninstall " + PackageName,
|
|
"echo",
|
|
"echo Removing old data. Failures here are usually fine - indicating the files were not on the device.",
|
|
"$ADB $DEVICE shell 'rm -r $EXTERNAL_STORAGE/UnrealGame/" + Params.ShortProjectName + "'",
|
|
"$ADB $DEVICE shell 'rm -r $EXTERNAL_STORAGE/UnrealGame/UECommandLine.txt" + "'",
|
|
"$ADB $DEVICE shell 'rm -r $EXTERNAL_STORAGE/" + TargetAndroidLocation + PackageName + "'",
|
|
"$ADB $DEVICE shell 'rm -r $EXTERNAL_STORAGE/Android/" + TargetAndroidLocation + PackageName + "'",
|
|
"echo",
|
|
"echo Uninstall completed",
|
|
"exit 0",
|
|
};
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("Writing bat for uninstall with {Arg0}", bPackageDataInsideApk ? "data in APK" : "separate OBB");
|
|
BatchLines = new string[] {
|
|
"setlocal",
|
|
"if NOT \"%UE_SDKS_ROOT%\"==\"\" (call %UE_SDKS_ROOT%\\HostWin64\\Android\\SetupEnvironmentVars.bat)",
|
|
"set ANDROIDHOME=%ANDROID_HOME%",
|
|
"if \"%ANDROIDHOME%\"==\"\" set ANDROIDHOME="+Environment.GetEnvironmentVariable("ANDROID_HOME"),
|
|
"set ADB=%ANDROIDHOME%\\platform-tools\\adb.exe",
|
|
"set DEVICE=",
|
|
"if not \"%1\"==\"\" set DEVICE=-s %1",
|
|
"for /f \"delims=\" %%A in ('%ADB% %DEVICE% " + GetStorageQueryCommand(true) +"') do @set STORAGE=%%A",
|
|
"@echo.",
|
|
"@echo Uninstalling existing application. Failures here can almost always be ignored.",
|
|
"%ADB% %DEVICE% uninstall " + PackageName,
|
|
"@echo.",
|
|
"echo Removing old data. Failures here are usually fine - indicating the files were not on the device.",
|
|
"%ADB% %DEVICE% shell rm -r %STORAGE%/UnrealGame/" + Params.ShortProjectName,
|
|
"%ADB% %DEVICE% shell rm -r %STORAGE%/UnrealGame/UECommandLine.txt", // we need to delete the commandline in UnrealGame or it will mess up loading
|
|
"%ADB% %DEVICE% shell rm -r %STORAGE%/" + TargetAndroidLocation + PackageName,
|
|
"%ADB% %DEVICE% shell rm -r %STORAGE%/Android/" + TargetAndroidLocation + PackageName,
|
|
"@echo.",
|
|
"@echo Uninstall completed",
|
|
};
|
|
}
|
|
return BatchLines;
|
|
}
|
|
|
|
private string[] GenerateSymbolizeBatchFile(ProjectParams Params, string PackageName, string ApkName, DeploymentContext SC, UnrealArch Architecture, bool bIsPC)
|
|
{
|
|
string[] BatchLines = null;
|
|
|
|
if (!bIsPC)
|
|
{
|
|
Logger.LogInformation("Writing shell script for symbolize with {Arg0}", "data in APK" );
|
|
BatchLines = new string[] {
|
|
"#!/bin/sh",
|
|
"if [ $? -ne 0]; then",
|
|
"echo \"Required argument missing, pass a dump of adb crash log.\"",
|
|
"exit 1",
|
|
"fi",
|
|
"cd \"`dirname \"$0\"`\"",
|
|
"NDKSTACK=",
|
|
"if [ \"$ANDROID_NDK_ROOT\" != \"\" ]; then NDKSTACK=$%ANDROID_NDK_ROOT/ndk-stack; else ADB=" + Environment.GetEnvironmentVariable("ANDROID_NDK_ROOT") + "/ndk-stack; fi",
|
|
"$NDKSTACK -sym " + GetFinalSymbolizedSODirectory(ApkName, SC, Architecture) + " -dump \"%1\" > " + Params.ShortProjectName + "_SymbolizedCallStackOutput.txt",
|
|
"exit 0",
|
|
};
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("Writing bat for symbolize");
|
|
BatchLines = new string[] {
|
|
"@echo off",
|
|
"IF %1.==. GOTO NoArgs",
|
|
"setlocal",
|
|
"set NDK_ROOT=%ANDROID_NDK_ROOT%",
|
|
"if \"%ANDROID_NDK_ROOT%\"==\"\" set NDK_ROOT=\""+Environment.GetEnvironmentVariable("ANDROID_NDK_ROOT")+"\"",
|
|
"set NDKSTACK=%NDK_ROOT%\\ndk-stack.cmd",
|
|
"",
|
|
"%NDKSTACK% -sym "+GetFinalSymbolizedSODirectory(ApkName, SC, Architecture)+" -dump \"%1\" > "+ Params.ShortProjectName+"_SymbolizedCallStackOutput.txt",
|
|
"",
|
|
"goto:eof",
|
|
"",
|
|
"",
|
|
":NoArgs",
|
|
"echo.",
|
|
"echo Required argument missing, pass a dump of adb crash log. (SymboliseCallStackDump C:\\adbcrashlog.txt)",
|
|
"pause"
|
|
};
|
|
}
|
|
return BatchLines;
|
|
}
|
|
|
|
public override void GetFilesToArchive(ProjectParams Params, DeploymentContext SC)
|
|
{
|
|
if (SC.StageTargetConfigurations.Count != 1)
|
|
{
|
|
throw new AutomationException(ExitCode.Error_OnlyOneTargetConfigurationSupported, "Android is currently only able to package one target configuration at a time, but StageTargetConfigurations contained {0} configurations", SC.StageTargetConfigurations.Count);
|
|
}
|
|
|
|
UnrealTargetConfiguration TargetConfiguration = SC.StageTargetConfigurations[0];
|
|
UnrealArchitectures Architectures = GetDeploymentArchitectures(Params, SC);
|
|
bool bMakeSeparateApks = UnrealBuildTool.AndroidExports.ShouldMakeSeparateApks();
|
|
bool bPackageDataInsideApk = UnrealBuildTool.AndroidExports.CreateDeploymentHandler(Params.RawProjectPath, Params.ForcePackageData).GetPackageDataInsideApk();
|
|
bool bBundleEnabled = GetEnableBundle(SC);
|
|
|
|
bool bAFSEnablePlugin;
|
|
string AFSToken;
|
|
bool bIsShipping;
|
|
bool bAFSIncludeInShipping;
|
|
bool bAFSAllowExternalStartInShipping;
|
|
UsingAndroidFileServer(Params, SC, out bAFSEnablePlugin, out AFSToken, out bIsShipping, out bAFSIncludeInShipping, out bAFSAllowExternalStartInShipping);
|
|
bool bUseAFS = bAFSEnablePlugin && !bPackageDataInsideApk;
|
|
int AllowOverflowOBBLimit = AllowOverflowOBBFiles(SC);
|
|
|
|
List<string> AddedObbFiles = new List<string>();
|
|
foreach (UnrealArch Architecture in Architectures.Architectures)
|
|
{
|
|
string ApkBareName = GetFinalApkName(Params, SC.StageExecutables[0], true, null);
|
|
string ApkName = GetFinalApkName(Params, SC.StageExecutables[0], true, bMakeSeparateApks ? Architecture : null);
|
|
bool bHaveAPK = FileExists(ApkName);
|
|
string ObbName = GetFinalObbName(ApkName, SC);
|
|
string PatchName = GetFinalPatchName(ApkName, SC);
|
|
bool bBuildWithHiddenSymbolVisibility = BuildWithHiddenSymbolVisibility(SC);
|
|
bool bSaveSymbols = GetSaveSymbols(SC);
|
|
|
|
string APKDirectory = Path.GetDirectoryName(ApkName);
|
|
string APKNameWithoutExtension = Path.GetFileNameWithoutExtension(ApkName);
|
|
string APKBareNameWithoutExtension = Path.GetFileNameWithoutExtension(ApkBareName);
|
|
|
|
bool bHaveAAB = false;
|
|
bool bHaveUniversal = false;
|
|
|
|
string AppBundleName = Path.Combine(APKDirectory, APKNameWithoutExtension + ".aab");
|
|
string APKSName = Path.Combine(APKDirectory, APKNameWithoutExtension + ".apks");
|
|
string UniversalApkName = Path.Combine(APKDirectory, APKNameWithoutExtension + "_universal.apk");
|
|
if (bBundleEnabled)
|
|
{
|
|
// copy optional app bundle if exists
|
|
if (FileExists(AppBundleName))
|
|
{
|
|
bHaveAAB = true;
|
|
SC.ArchiveFiles(APKDirectory, Path.GetFileName(AppBundleName));
|
|
}
|
|
else
|
|
{
|
|
AppBundleName = Path.Combine(APKDirectory, APKBareNameWithoutExtension + ".aab");
|
|
if (FileExists(AppBundleName))
|
|
{
|
|
bHaveAAB = true;
|
|
SC.ArchiveFiles(APKDirectory, Path.GetFileName(AppBundleName));
|
|
}
|
|
}
|
|
|
|
// copy optional apks (zip of split apks) if exists
|
|
if (FileExists(APKSName))
|
|
{
|
|
SC.ArchiveFiles(APKDirectory, Path.GetFileName(APKSName));
|
|
}
|
|
else
|
|
{
|
|
APKSName = Path.Combine(APKDirectory, APKBareNameWithoutExtension + ".apks");
|
|
if (FileExists(APKSName))
|
|
{
|
|
SC.ArchiveFiles(APKDirectory, Path.GetFileName(APKSName));
|
|
}
|
|
}
|
|
|
|
// copy optional universal apk if exists
|
|
if (FileExists(UniversalApkName))
|
|
{
|
|
bHaveUniversal = true;
|
|
SC.ArchiveFiles(APKDirectory, Path.GetFileName(UniversalApkName));
|
|
}
|
|
else
|
|
{
|
|
UniversalApkName = Path.Combine(APKDirectory, APKBareNameWithoutExtension + "_universal.apk");
|
|
if (FileExists(UniversalApkName))
|
|
{
|
|
bHaveUniversal = true;
|
|
SC.ArchiveFiles(APKDirectory, Path.GetFileName(UniversalApkName));
|
|
}
|
|
}
|
|
}
|
|
|
|
// copy optional AFSProject apk if exists
|
|
string AFSApkName = Path.Combine(APKDirectory, "AFS_" + APKNameWithoutExtension + ".apk");
|
|
if (FileExists(AFSApkName))
|
|
{
|
|
SC.ArchiveFiles(APKDirectory, Path.GetFileName(AFSApkName));
|
|
}
|
|
|
|
// add any other APKs with a prefix
|
|
IEnumerable<string> files = Directory.EnumerateFiles(APKDirectory, "*_" + APKNameWithoutExtension + ".apk", SearchOption.TopDirectoryOnly);
|
|
foreach (string filename in files)
|
|
{
|
|
if (filename != AFSApkName)
|
|
{
|
|
SC.ArchiveFiles(APKDirectory, Path.GetFileName(filename));
|
|
}
|
|
}
|
|
|
|
// verify the files exist
|
|
if (!FileExists(ApkName))
|
|
{
|
|
// still valid if we found an AAB
|
|
if (!bHaveAAB)
|
|
{
|
|
throw new AutomationException(ExitCode.Error_AppNotFound, "ARCHIVE FAILED - {0} was not found", ApkName);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SC.ArchiveFiles(Path.GetDirectoryName(ApkName), Path.GetFileName(ApkName));
|
|
}
|
|
|
|
if (!bPackageDataInsideApk && !FileExists(ObbName))
|
|
{
|
|
throw new AutomationException(ExitCode.Error_ObbNotFound, "ARCHIVE FAILED - {0} was not found", ObbName);
|
|
}
|
|
|
|
// Deprecated check of bBuildWithHiddenSymbolVisibility. Remove in 5.8+
|
|
if (bBuildWithHiddenSymbolVisibility || bSaveSymbols)
|
|
{
|
|
string SymbolizedSODirectory = GetFinalSymbolizedSODirectory(ApkName, SC, Architecture);
|
|
string SymbolizedSOPath = Path.Combine(Path.Combine(Path.GetDirectoryName(ApkName), SymbolizedSODirectory), "libUnreal.so");
|
|
if (!FileExists(SymbolizedSOPath))
|
|
{
|
|
throw new AutomationException(ExitCode.Error_SymbolizedSONotFound, "ARCHIVE FAILED - {0} was not found", SymbolizedSOPath);
|
|
}
|
|
// Add symbolized .so directory
|
|
SC.ArchiveFiles(Path.GetDirectoryName(SymbolizedSOPath), Path.GetFileName(SymbolizedSOPath), true, null, SymbolizedSODirectory);
|
|
|
|
// copy mapping.txt file if generated
|
|
string SymbolizedBasePath = SymbolizedSODirectory.Substring(0, SymbolizedSODirectory.LastIndexOf("/"));
|
|
string SymbolizedMappingFile = Path.Combine(Path.Combine(Path.GetDirectoryName(ApkName), SymbolizedBasePath), "mapping.txt");
|
|
if (FileExists(SymbolizedMappingFile))
|
|
{
|
|
SC.ArchiveFiles(Path.GetDirectoryName(SymbolizedMappingFile), Path.GetFileName(SymbolizedMappingFile), false, null, SymbolizedBasePath);
|
|
}
|
|
}
|
|
|
|
if (!bPackageDataInsideApk)
|
|
{
|
|
// only add if not already in archive list
|
|
if (!AddedObbFiles.Contains(ObbName))
|
|
{
|
|
AddedObbFiles.Add(ObbName);
|
|
|
|
SC.ArchiveFiles(Path.GetDirectoryName(ObbName), Path.GetFileName(ObbName));
|
|
if (FileExists(PatchName))
|
|
{
|
|
SC.ArchiveFiles(Path.GetDirectoryName(PatchName), Path.GetFileName(PatchName));
|
|
}
|
|
|
|
for (int Index = 1; Index <= AllowOverflowOBBLimit; Index++)
|
|
{
|
|
string OverflowName = GetFinalOverflowName(ApkName, SC, Index);
|
|
if (FileExists(OverflowName))
|
|
{
|
|
SC.ArchiveFiles(Path.GetDirectoryName(OverflowName), Path.GetFileName(OverflowName));
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// copy optional unprotected APK if exists
|
|
string UnprotectedApkName = Path.Combine(APKDirectory, "unprotected_" + APKNameWithoutExtension + ".apk");
|
|
if (FileExists(UnprotectedApkName))
|
|
{
|
|
SC.ArchiveFiles(APKDirectory, Path.GetFileName(UnprotectedApkName));
|
|
}
|
|
|
|
// copy optional logs directory if exists
|
|
string LogsDirName = Path.Combine(APKDirectory, APKNameWithoutExtension + ".logs");
|
|
if (DirectoryExists(LogsDirName))
|
|
{
|
|
SC.ArchiveFiles(LogsDirName);
|
|
}
|
|
|
|
bool bNeedsPCInstall = false;
|
|
bool bNeedsMacInstall = false;
|
|
bool bNeedsLinuxInstall = false;
|
|
GetPlatformInstallOptions(SC, out bNeedsPCInstall, out bNeedsMacInstall, out bNeedsLinuxInstall);
|
|
|
|
//helper delegate to prevent code duplication but allow us access to all the local variables we need
|
|
var CreateBatchFilesAndArchiveAction = new Action<UnrealTargetPlatform>(Target =>
|
|
{
|
|
if (bHaveAPK)
|
|
{
|
|
string BatchName = GetFinalBatchName(ApkName, SC, false, EBatchType.Install, Target);
|
|
SC.ArchiveFiles(Path.GetDirectoryName(BatchName), Path.GetFileName(BatchName));
|
|
}
|
|
if (bHaveAPK || bHaveUniversal)
|
|
{
|
|
string UninstallBatchName = GetFinalBatchName(ApkName, SC, false, EBatchType.Uninstall, Target);
|
|
SC.ArchiveFiles(Path.GetDirectoryName(UninstallBatchName), Path.GetFileName(UninstallBatchName));
|
|
}
|
|
if (bHaveUniversal)
|
|
{
|
|
string UniversalBatchName = GetFinalBatchName(UniversalApkName, SC, false, EBatchType.Install, Target);
|
|
SC.ArchiveFiles(Path.GetDirectoryName(UniversalBatchName), Path.GetFileName(UniversalBatchName));
|
|
}
|
|
|
|
// Deprecated check of bBuildWithHiddenSymbolVisibility. Remove in 5.8+
|
|
if (bBuildWithHiddenSymbolVisibility || bSaveSymbols)
|
|
{
|
|
string SymbolizeBatchName = GetFinalBatchName(ApkName, SC, false, EBatchType.Symbolize, Target);
|
|
SC.ArchiveFiles(Path.GetDirectoryName(SymbolizeBatchName), Path.GetFileName(SymbolizeBatchName));
|
|
}
|
|
if (bUseAFS && (bHaveAPK || bHaveUniversal))
|
|
{
|
|
SC.ArchiveFiles(Path.Combine(SC.EngineRoot.FullName, "Binaries", "DotNET", "Android", "UnrealAndroidFileTool"), GetAFSExecutable(Target));
|
|
}
|
|
//SC.ArchiveFiles(Path.GetDirectoryName(NoOBBBatchName), Path.GetFileName(NoOBBBatchName));
|
|
}
|
|
);
|
|
|
|
//it's possible we will need both PC and Mac/Linux install files, do both
|
|
if (bNeedsPCInstall)
|
|
{
|
|
CreateBatchFilesAndArchiveAction(UnrealTargetPlatform.Win64);
|
|
}
|
|
if (bNeedsMacInstall)
|
|
{
|
|
CreateBatchFilesAndArchiveAction(UnrealTargetPlatform.Mac);
|
|
}
|
|
if (bNeedsLinuxInstall)
|
|
{
|
|
CreateBatchFilesAndArchiveAction(UnrealTargetPlatform.Linux);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void GetPlatformInstallOptions(DeploymentContext SC, out bool bNeedsPCInstall, out bool bNeedsMacInstall, out bool bNeedsLinuxInstall)
|
|
{
|
|
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(SC.RawProjectPath), SC.StageTargetPlatform.PlatformType);
|
|
bool bGenerateAllPlatformInstall = false;
|
|
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bCreateAllPlatformsInstall", out bGenerateAllPlatformInstall);
|
|
|
|
bNeedsPCInstall = bNeedsMacInstall = bNeedsLinuxInstall = false;
|
|
|
|
if (bGenerateAllPlatformInstall)
|
|
{
|
|
bNeedsPCInstall = bNeedsMacInstall = bNeedsLinuxInstall = true;
|
|
}
|
|
else
|
|
{
|
|
if (HostPlatform.Current.HostEditorPlatform == UnrealTargetPlatform.Mac)
|
|
{
|
|
bNeedsMacInstall = true;
|
|
}
|
|
else if (HostPlatform.Current.HostEditorPlatform == UnrealTargetPlatform.Linux)
|
|
{
|
|
bNeedsLinuxInstall = true;
|
|
}
|
|
else
|
|
{
|
|
bNeedsPCInstall = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string GetAdbCommandLine(string SerialNumber, string Args)
|
|
{
|
|
if (string.IsNullOrEmpty(SerialNumber) == false)
|
|
{
|
|
SerialNumber = "-s " + SerialNumber;
|
|
}
|
|
|
|
return string.Format("{0} {1}", SerialNumber, Args);
|
|
}
|
|
private static string GetAFSCommandLine(string SerialNumber, string Args)
|
|
{
|
|
if (string.IsNullOrEmpty(SerialNumber) == false)
|
|
{
|
|
SerialNumber = "-s " + SerialNumber;
|
|
}
|
|
|
|
return string.Format("{0} {1}", SerialNumber, Args);
|
|
}
|
|
|
|
static string LastSpewFilename = "";
|
|
|
|
public static string ADBSpewFilter(string Message)
|
|
{
|
|
if (Message.StartsWith("[") && Message.Contains("%]"))
|
|
{
|
|
int LastIndex = Message.IndexOf(":");
|
|
LastIndex = LastIndex == -1 ? Message.Length : LastIndex;
|
|
|
|
if (Message.Length > 7)
|
|
{
|
|
string Filename = Message.Substring(7, LastIndex - 7);
|
|
if (Filename == LastSpewFilename)
|
|
{
|
|
return null;
|
|
}
|
|
LastSpewFilename = Filename;
|
|
}
|
|
return Message;
|
|
}
|
|
return Message;
|
|
}
|
|
|
|
public static IProcessResult RunAdbCommand(ProjectParams Params, string SerialNumber, string Args, string Input = null, ERunOptions Options = ERunOptions.Default, bool bShouldLogCommand = false)
|
|
{
|
|
return RunAdbCommand(SerialNumber, Args, Input, Options, bShouldLogCommand);
|
|
}
|
|
|
|
private static IProcessResult RunAdbCommand(string SerialNumber, string Args, string Input = null, ERunOptions Options = ERunOptions.Default, bool bShouldLogCommand = false)
|
|
{
|
|
string AdbCommand = Environment.ExpandEnvironmentVariables("%ANDROID_HOME%/platform-tools/adb" + (OperatingSystem.IsWindows() ? ".exe" : ""));
|
|
if (Options.HasFlag(ERunOptions.AllowSpew) || Options.HasFlag(ERunOptions.SpewIsVerbose))
|
|
{
|
|
LastSpewFilename = "";
|
|
return Run(AdbCommand, GetAdbCommandLine(SerialNumber, Args), Input, Options, SpewFilterCallback: new ProcessResult.SpewFilterCallbackType(ADBSpewFilter));
|
|
}
|
|
return Run(AdbCommand, GetAdbCommandLine(SerialNumber, Args), Input, Options);
|
|
}
|
|
|
|
private string RunAndLogAdbCommand(ProjectParams Params, string SerialNumber, string Args, out int SuccessCode)
|
|
{
|
|
string AdbCommand = Environment.ExpandEnvironmentVariables("%ANDROID_HOME%/platform-tools/adb" + (OperatingSystem.IsWindows() ? ".exe" : ""));
|
|
LastSpewFilename = "";
|
|
return RunAndLog(CmdEnv, AdbCommand, GetAdbCommandLine(SerialNumber, Args), out SuccessCode, SpewFilterCallback: new ProcessResult.SpewFilterCallbackType(ADBSpewFilter));
|
|
}
|
|
|
|
public static IProcessResult RunAFSCommand(ProjectParams Params, string SerialNumber, string Args, string Input = null, ERunOptions Options = ERunOptions.Default, bool bShouldLogCommand = false)
|
|
{
|
|
return RunAFSCommand(SerialNumber, Args, Input, Options, bShouldLogCommand);
|
|
}
|
|
|
|
private static IProcessResult RunAFSCommand(string SerialNumber, string Args, string Input = null, ERunOptions Options = ERunOptions.Default, bool bShouldLogCommand = false)
|
|
{
|
|
string AFSExecutable = AndroidExports.GetAFSExecutable(BuildHostPlatform.Current.Platform, EpicGames.Core.Log.Logger);
|
|
|
|
AFSExecutable = Path.Combine(Unreal.EngineDirectory.FullName, "Binaries", "DotNET", "Android", "UnrealAndroidFileTool", AFSExecutable);
|
|
if (Options.HasFlag(ERunOptions.AllowSpew) || Options.HasFlag(ERunOptions.SpewIsVerbose))
|
|
{
|
|
LastSpewFilename = "";
|
|
return Run(AFSExecutable, GetAFSCommandLine(SerialNumber, Args), Input, Options, SpewFilterCallback: new ProcessResult.SpewFilterCallbackType(ADBSpewFilter));
|
|
}
|
|
return Run(AFSExecutable, GetAFSCommandLine(SerialNumber, Args), Input, Options);
|
|
}
|
|
|
|
private string RunAndLogAFSCommand(ProjectParams Params, string SerialNumber, string Args, out int SuccessCode)
|
|
{
|
|
string AFSExecutable = AndroidExports.GetAFSExecutable(BuildHostPlatform.Current.Platform, EpicGames.Core.Log.Logger);
|
|
AFSExecutable = Path.Combine(Unreal.EngineDirectory.FullName, "Binaries", "DotNET", "Android", "UnrealAndroidFileTool", AFSExecutable);
|
|
LastSpewFilename = "";
|
|
return RunAndLog(CmdEnv, AFSExecutable, GetAFSCommandLine(SerialNumber, Args), out SuccessCode, SpewFilterCallback: new ProcessResult.SpewFilterCallbackType(ADBSpewFilter));
|
|
}
|
|
|
|
public override void GetConnectedDevices(ProjectParams Params, out List<string> Devices)
|
|
{
|
|
Devices = new List<string>();
|
|
IProcessResult Result = RunAdbCommand(Params, "", "devices");
|
|
|
|
if (Result.Output.Length > 0)
|
|
{
|
|
string[] LogLines = Result.Output.Split(new char[] { '\n', '\r' });
|
|
bool FoundList = false;
|
|
for (int i = 0; i < LogLines.Length; ++i)
|
|
{
|
|
if (FoundList == false)
|
|
{
|
|
if (LogLines[i].StartsWith("List of devices attached"))
|
|
{
|
|
FoundList = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
string[] DeviceLine = LogLines[i].Split(new char[] { '\t' });
|
|
|
|
if (DeviceLine.Length == 2)
|
|
{
|
|
// the second param should be "device"
|
|
// if it's not setup correctly it might be "unattached" or "powered off" or something like that
|
|
// warning in that case
|
|
if (DeviceLine[1] == "device")
|
|
{
|
|
Devices.Add("@" + DeviceLine[0]);
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning("Device attached but in bad state {Arg0}:{Arg1}", DeviceLine[0], DeviceLine[1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
private class TimeRegion : System.IDisposable
|
|
{
|
|
private System.DateTime StartTime { get; set; }
|
|
|
|
private string Format { get; set; }
|
|
|
|
private System.Collections.Generic.List<object> FormatArgs { get; set; }
|
|
|
|
public TimeRegion(string format, params object[] format_args)
|
|
{
|
|
Format = format;
|
|
FormatArgs = new List<object>(format_args);
|
|
StartTime = DateTime.UtcNow;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
double total_time = (DateTime.UtcNow - StartTime).TotalMilliseconds / 1000.0;
|
|
FormatArgs.Insert(0, total_time);
|
|
CommandUtils.Log(Format, FormatArgs.ToArray());
|
|
}
|
|
}
|
|
*/
|
|
|
|
private bool RetrieveDeployedManifestsAFS(ProjectParams Params, DeploymentContext SC, string DeviceName, out List<string> UFSManifests, out List<string> NonUFSManifests, string AFSToken)
|
|
{
|
|
UFSManifests = null;
|
|
NonUFSManifests = null;
|
|
|
|
UnrealArch? DeviceArchitecture = GetBestDeviceArchitecture(Params, DeviceName);
|
|
string ApkName = GetFinalApkName(Params, SC.StageExecutables[0], true, DeviceArchitecture);
|
|
string PackageName = GetPackageInfo(ApkName, SC, false);
|
|
|
|
AndroidFileClient client = new AndroidFileClient(DeviceName);
|
|
if (!client.OpenConnection())
|
|
{
|
|
if (PackageName == null || PackageName == "")
|
|
{
|
|
Logger.LogInformation("Retrieve Manifests: Unable to start file server without package name, ignoring manifests");
|
|
return false;
|
|
}
|
|
Logger.LogInformation("Retrieve Manifests: Trying to start file server {PackageName}", PackageName);
|
|
if (!client.StartServer(PackageName, AFSToken))
|
|
{
|
|
Logger.LogInformation("Retrieve Manifests: Failed to start server, ignoring manifests");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// verify connection to the correct server
|
|
string DevicePackageName = client.Query("^packagename");
|
|
if (DevicePackageName != PackageName)
|
|
{
|
|
if (PackageName == null || PackageName == "")
|
|
{
|
|
Logger.LogInformation("Retrieve Manifests: Unable to start file server without package name, ignoring manifests");
|
|
client.CloseConnection();
|
|
return false;
|
|
}
|
|
|
|
Logger.LogInformation("Connected to wrong server {DevicePackageName}, trying again", DevicePackageName);
|
|
client.TerminateServer();
|
|
|
|
Logger.LogInformation("Retrieve Manifests: Trying to start file server {PackageName}", PackageName);
|
|
if (!client.StartServer(PackageName, AFSToken))
|
|
{
|
|
Logger.LogInformation("Retrieve Manifests: Failed to start server, ignoring manifests");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Try retrieving the UFS files manifest files from the device
|
|
string UFSManifestFileName = CombinePaths(SC.StageDirectory.FullName, SC.GetUFSDeployedManifestFileName(DeviceName));
|
|
if (!client.FileRead("^project/" + SC.GetUFSDeployedManifestFileName(null), UFSManifestFileName))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Try retrieving the non UFS files manifest files from the device
|
|
string NonUFSManifestFileName = CombinePaths(SC.StageDirectory.FullName, SC.GetNonUFSDeployedManifestFileName(DeviceName));
|
|
if (!client.FileRead("^project/" + SC.GetNonUFSDeployedManifestFileName(null), NonUFSManifestFileName))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
client.CloseConnection();
|
|
|
|
// Return the manifest files
|
|
UFSManifests = new List<string>();
|
|
UFSManifests.Add(UFSManifestFileName);
|
|
NonUFSManifests = new List<string>();
|
|
NonUFSManifests.Add(NonUFSManifestFileName);
|
|
|
|
Logger.LogInformation("Retrieve Manifests: Success!!");
|
|
|
|
return true;
|
|
}
|
|
|
|
public override bool RetrieveDeployedManifests(ProjectParams Params, DeploymentContext SC, string DeviceName, out List<string> UFSManifests, out List<string> NonUFSManifests)
|
|
{
|
|
DeviceName = GetSerialNumber(DeviceName);
|
|
|
|
bool bAFSEnablePlugin;
|
|
string AFSToken;
|
|
bool bIsShipping;
|
|
bool bAFSIncludeInShipping;
|
|
bool bAFSAllowExternalStartInShipping;
|
|
if (UsingAndroidFileServer(Params, SC, out bAFSEnablePlugin, out AFSToken, out bIsShipping, out bAFSIncludeInShipping, out bAFSAllowExternalStartInShipping))
|
|
{
|
|
return RetrieveDeployedManifestsAFS(Params, SC, DeviceName, out UFSManifests, out NonUFSManifests, AFSToken);
|
|
}
|
|
|
|
UFSManifests = null;
|
|
NonUFSManifests = null;
|
|
|
|
// Query the storage path from the device
|
|
string DeviceStorageQueryCommand = GetStorageQueryCommand();
|
|
IProcessResult StorageResult = RunAdbCommand(Params, DeviceName, DeviceStorageQueryCommand, null, ERunOptions.AppMustExist);
|
|
String StorageLocation = StorageResult.Output.Trim();
|
|
string RemoteDir = StorageLocation + "/UnrealGame/" + Params.ShortProjectName;
|
|
|
|
// Try retrieving the UFS files manifest files from the device
|
|
string RetrievedUFSManifestFileName = CombinePaths(SC.StageDirectory.FullName, $"Retrieved_{SC.GetUFSDeployedManifestFileName(DeviceName)}");
|
|
IProcessResult UFSResult = RunAdbCommand(Params, DeviceName, " pull " + RemoteDir + "/" + SC.GetUFSDeployedManifestFileName(null) + " \"" + RetrievedUFSManifestFileName + "\"", null, ERunOptions.AppMustExist);
|
|
if (!(UFSResult.Output.Contains("bytes") || UFSResult.Output.Contains("[100%]")))
|
|
{
|
|
Logger.LogWarning("Failed retrieving UFS Manifest: {Arg0}", UFSResult.Output);
|
|
return false;
|
|
}
|
|
|
|
// Try retrieving the non UFS files manifest files from the device
|
|
string RetrievedNonUFSManifestFileName = CombinePaths(SC.StageDirectory.FullName, $"Retrieved_{SC.GetNonUFSDeployedManifestFileName(DeviceName)}");
|
|
IProcessResult NonUFSResult = RunAdbCommand(Params, DeviceName, " pull " + RemoteDir + "/" + SC.GetNonUFSDeployedManifestFileName(null) + " \"" + RetrievedNonUFSManifestFileName + "\"", null, ERunOptions.AppMustExist);
|
|
if (!(NonUFSResult.Output.Contains("bytes") || NonUFSResult.Output.Contains("[100%]")))
|
|
{
|
|
Logger.LogWarning("Failed retrieving NonUFS Manifest: {Arg0}", NonUFSResult.Output);
|
|
// Did not retrieve both so delete one we did retrieve
|
|
File.Delete(RetrievedUFSManifestFileName);
|
|
return false;
|
|
}
|
|
|
|
// Return the manifest files
|
|
UFSManifests = new List<string>();
|
|
UFSManifests.Add(RetrievedUFSManifestFileName);
|
|
NonUFSManifests = new List<string>();
|
|
NonUFSManifests.Add(RetrievedNonUFSManifestFileName);
|
|
|
|
return true;
|
|
}
|
|
|
|
internal class LongestFirst : IComparer<string>
|
|
{
|
|
public int Compare(string a, string b)
|
|
{
|
|
if (a.Length == b.Length) return a.CompareTo(b);
|
|
else return b.Length - a.Length;
|
|
}
|
|
}
|
|
|
|
// Returns a filename from "adb shell ls -RF" output
|
|
// or null if the input line is a directory.
|
|
private string GetFileNameFromListing(string SingleLine)
|
|
{
|
|
if (SingleLine.StartsWith("- ")) // file on Samsung
|
|
return SingleLine.Substring(2);
|
|
else if (SingleLine.StartsWith("d ")) // directory on Samsung
|
|
return null;
|
|
else if (SingleLine.EndsWith("/")) // directory on Google
|
|
return null;
|
|
else // undecorated = file on Google
|
|
{
|
|
return SingleLine;
|
|
}
|
|
}
|
|
|
|
private bool GetDontBundleLibrariesInAPK(ProjectParams Params, DeploymentContext SC, bool bVerbose = false)
|
|
{
|
|
return AndroidExports.GetDontBundleLibrariesInAPK(Params.RawProjectPath, null, SC.StageTargets[0].Receipt.Configuration, SC.Archive, false,
|
|
true, bVerbose ? Logger : null);
|
|
}
|
|
|
|
private void DeployAndroidFileServer(ProjectParams Params, DeploymentContext SC, string AFSToken)
|
|
{
|
|
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(Params.RawProjectPath), UnrealTargetPlatform.Android);
|
|
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bEnableMaliPerfCounters", out bool bDisablePerfHarden);
|
|
bool bDontBundleLibrariesInAPK = GetDontBundleLibrariesInAPK(Params, SC, true);
|
|
|
|
bool bUseCompression;
|
|
bool bLogFiles;
|
|
bool bReportStats;
|
|
bool bUseManualIPAddress;
|
|
string ManualIPAddress;
|
|
EConnectionType ConnectionType = GetAndroidFileServerNetworkConfig(SC, out bUseCompression, out bLogFiles, out bReportStats, out bUseManualIPAddress, out ManualIPAddress);
|
|
|
|
AndroidFileClient.OptimalADB adb = new AndroidFileClient.OptimalADB();
|
|
int AllowOverflowOBBLimit = AllowOverflowOBBFiles(SC);
|
|
|
|
foreach (string DeviceName in Params.DeviceNames.Select(DeviceName => GetSerialNumber(DeviceName)))
|
|
{
|
|
UnrealArch? DeviceArchitecture = GetBestDeviceArchitecture(Params, DeviceName);
|
|
string ApkName = GetFinalApkName(Params, SC.StageExecutables[0], true, DeviceArchitecture);
|
|
string FinalSOName = GetSOName(Params, SC.StageExecutables[0], DeviceArchitecture);
|
|
|
|
// make sure APK is up to date (this is fast if so)
|
|
var Deploy = AndroidExports.CreateDeploymentHandler(Params.RawProjectPath, Params.ForcePackageData);
|
|
if (!Params.Prebuilt)
|
|
{
|
|
string CookFlavor = SC.FinalCookPlatform.IndexOf("_") > 0 ? SC.FinalCookPlatform.Substring(SC.FinalCookPlatform.IndexOf("_")) : "";
|
|
string SOName = GetSONameWithoutArchitecture(Params, SC.StageExecutables[0]);
|
|
Deploy.SetAndroidPluginData(GetDeploymentArchitectures(Params, SC), CollectPluginDataPaths(SC));
|
|
Deploy.PrepForUATPackageOrDeploy(Params.RawProjectPath, Params.ShortProjectName, SC.ProjectRoot, SOName, SC.LocalRoot + "/Engine", Params.Distribution, CookFlavor, SC.StageTargets[0].Receipt.Configuration, true, false, SC.Archive);
|
|
}
|
|
|
|
// now we can use the apk to get more info
|
|
string PackageName = GetPackageInfo(ApkName, SC, false);
|
|
|
|
// start up AFS connection (allowed to fail here.. may not be a server installed yet)
|
|
AndroidFileClient client = new AndroidFileClient(DeviceName);
|
|
AndroidFileClient client2 = null;
|
|
string IPAddress = (ConnectionType == EConnectionType.NetworkOnly && bUseManualIPAddress) ? ManualIPAddress : "127.0.0.1";
|
|
if (!client.OpenConnection(IPAddress))
|
|
{
|
|
if (!client.StartServer(PackageName, AFSToken, IPAddress))
|
|
{
|
|
}
|
|
}
|
|
|
|
// Try to get IP address from device if we connected
|
|
string DeviceIPAddress = client.Query("^ip");
|
|
|
|
// Setup the OBB name and add the storage path (queried from the device) to it
|
|
string QueryStorageResult = adb.Shell(DeviceName, "echo $EXTERNAL_STORAGE");
|
|
string ExternalStorage = QueryStorageResult.Trim(); // "mnt/sdcard"
|
|
string StorageLocation = ExternalStorage + "/Android"; // "mnt/sdcard/Android"
|
|
string DeviceObbName = StorageLocation + "/" + GetDeviceObbName(ApkName, SC);
|
|
string DevicePatchName = StorageLocation + "/" + GetDevicePatchName(ApkName, SC);
|
|
string RemoteDir = client.Query("^project", true);
|
|
if (RemoteDir == null)
|
|
{
|
|
RemoteDir = StorageLocation + "/data/" + PackageName + "/files/UnrealGame/" + Params.ShortProjectName;
|
|
}
|
|
string ExtFiles = StorageLocation + "/data/" + PackageName + "/files";
|
|
|
|
if (bDisablePerfHarden)
|
|
{
|
|
adb.Shell(DeviceName, "setprop security.perf_harden 0");
|
|
}
|
|
|
|
// remove any main or patch OBB file which would override deployed data (note: not the same as later delete or deploy)
|
|
{
|
|
string DeviceOldObbName = ExternalStorage + "/" + GetDeviceObbName(ApkName, SC);
|
|
string DeviceOldPatchName = ExternalStorage + "/" + GetDevicePatchName(ApkName, SC);
|
|
|
|
adb.Shell(DeviceName, "rm " + DeviceOldObbName);
|
|
adb.Shell(DeviceName, "rm " + DeviceOldPatchName);
|
|
}
|
|
|
|
// close connection to since uninstall/reinstall will reset server
|
|
client.CloseConnection();
|
|
|
|
// determine if APK out of date
|
|
string APKLastUpdateTime = new FileInfo(ApkName).LastWriteTime.ToString();
|
|
bool bNeedAPKInstall = true;
|
|
bool bFreshInstall = false;
|
|
bool bFastDeploy = false;
|
|
if (Params.IterativeDeploy)
|
|
{
|
|
// Check for apk installed with this package name on the device
|
|
String InstalledResult = adb.Shell(DeviceName, "pm list packages " + PackageName);
|
|
if (InstalledResult.Contains(PackageName))
|
|
{
|
|
Logger.LogInformation("{PackageName} already installed!", PackageName);
|
|
// already installed so enable --fast-deploy option if need to update apk
|
|
bFastDeploy = true;
|
|
|
|
// See if apk is up to date on device
|
|
InstalledResult = adb.Shell(DeviceName, "cat " + ExtFiles + "/APKFileStamp.txt");
|
|
if (InstalledResult.StartsWith("APK: "))
|
|
{
|
|
Logger.LogInformation("Found APKFileStamp.txt! {InstalledResult}", InstalledResult);
|
|
if (InstalledResult.Substring(5).Trim() == APKLastUpdateTime)
|
|
bNeedAPKInstall = false;
|
|
|
|
if (InstalledResult.Substring(5).Trim() != APKLastUpdateTime)
|
|
{
|
|
Logger.LogInformation("{Arg0} != {APKLastUpdateTime}", InstalledResult.Substring(5).Trim(), APKLastUpdateTime);
|
|
}
|
|
|
|
// Stop the previously running copy (uninstall/install did this before)
|
|
InstalledResult = adb.Shell(DeviceName, "am force-stop " + PackageName);
|
|
if (InstalledResult.Contains("Error"))
|
|
{
|
|
// force-stop not supported (Android < 3.0) so check if package is actually running
|
|
// Note: cannot use grep here since it may not be installed on device
|
|
InstalledResult = adb.Shell(DeviceName, "ps");
|
|
if (InstalledResult.Contains(PackageName))
|
|
{
|
|
// it is actually running so use the slow way to kill it (uninstall and reinstall)
|
|
bNeedAPKInstall = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If not already installed we must do a full deploy
|
|
bFreshInstall = true;
|
|
}
|
|
}
|
|
|
|
// install new APK if needed
|
|
if (bNeedAPKInstall)
|
|
{
|
|
// try reinstall the apk to preserve data
|
|
int SuccessCode = 0;
|
|
string InstallCommandline = "install -r " + (bFastDeploy ? "--fastdeploy \"" : "\"") + ApkName + "\"";
|
|
string InstallOutput = RunAndLogAdbCommand(Params, DeviceName, InstallCommandline, out SuccessCode);
|
|
int FailureIndex = InstallOutput.IndexOf("Failure");
|
|
// adb install doesn't always return an error code on failure, and instead prints "Failure", followed by an error code.
|
|
if (SuccessCode != 0 || FailureIndex != -1)
|
|
{
|
|
string ErrorMessage = string.Format("Installation of apk '{0}' failed", ApkName);
|
|
if (FailureIndex != -1)
|
|
{
|
|
string FailureString = InstallOutput.Substring(FailureIndex + 7).Trim();
|
|
if (FailureString != "")
|
|
{
|
|
ErrorMessage += ": " + FailureString;
|
|
}
|
|
}
|
|
if (ErrorMessage.Contains("OLDER_SDK"))
|
|
{
|
|
Logger.LogError("minSdkVersion is higher than Android version installed on device, possibly due to NDK API Level");
|
|
throw new AutomationException(ExitCode.Error_AppInstallFailed, ErrorMessage);
|
|
}
|
|
|
|
// try uninstalling an old app with the same identifier.
|
|
// NOTE: uninstall -k will preserve data/cache.. consider using instead?
|
|
bFreshInstall = true;
|
|
SuccessCode = 0;
|
|
adb.Shell(DeviceName, "pm uninstall " + PackageName);
|
|
|
|
// install the apk
|
|
InstallCommandline = "install \"" + ApkName + "\"";
|
|
InstallOutput = RunAndLogAdbCommand(Params, DeviceName, InstallCommandline, out SuccessCode);
|
|
FailureIndex = InstallOutput.IndexOf("Failure");
|
|
|
|
// adb install doesn't always return an error code on failure, and instead prints "Failure", followed by an error code.
|
|
if (SuccessCode != 0 || FailureIndex != -1)
|
|
{
|
|
ErrorMessage = string.Format("Installation of apk '{0}' failed", ApkName);
|
|
if (FailureIndex != -1)
|
|
{
|
|
string FailureString = InstallOutput.Substring(FailureIndex + 7).Trim();
|
|
if (FailureString != "")
|
|
{
|
|
ErrorMessage += ": " + FailureString;
|
|
}
|
|
}
|
|
if (ErrorMessage.Contains("OLDER_SDK"))
|
|
{
|
|
Logger.LogError("minSdkVersion is higher than Android version installed on device, possibly due to NDK API Level");
|
|
}
|
|
throw new AutomationException(ExitCode.Error_AppInstallFailed, ErrorMessage);
|
|
}
|
|
}
|
|
|
|
// giving EXTERNAL_STORAGE_WRITE permission to the apk for API23+
|
|
// without this permission apk can't access to the assets put into the device
|
|
string ReadPermissionCommandLine = "pm grant " + PackageName + " android.permission.READ_EXTERNAL_STORAGE";
|
|
string WritePermissionCommandLine = "pm grant " + PackageName + " android.permission.WRITE_EXTERNAL_STORAGE";
|
|
adb.Shell(DeviceName, ReadPermissionCommandLine);
|
|
adb.Shell(DeviceName, WritePermissionCommandLine);
|
|
|
|
// grant permission for the foreground service to start (otherwise a security violation on Android 28+)
|
|
string ForegroundPermissionGrantCommand = "pm grant " + PackageName + " android.permission.FOREGROUND_SERVICE";
|
|
string ForegroundDataSyncPermissionGrantCommand = "pm grant " + PackageName + " android.permission.FOREGROUND_SERVICE_DATA_SYNC";
|
|
string NotificationsGrantCommand = "pm grant " + PackageName + " android.permission.POST_NOTIFICATIONS";
|
|
adb.Shell(DeviceName, ForegroundPermissionGrantCommand);
|
|
adb.Shell(DeviceName, ForegroundDataSyncPermissionGrantCommand);
|
|
adb.Shell(DeviceName, NotificationsGrantCommand);
|
|
|
|
// time for receivers to be registered by pm after install
|
|
Thread.Sleep(350);
|
|
}
|
|
|
|
// reopen file server connection (either USB or Network)
|
|
IPAddress = (ConnectionType == EConnectionType.NetworkOnly) ? (bUseManualIPAddress ? ManualIPAddress : (DeviceIPAddress != null ? DeviceIPAddress : "127.0.0.1")) : "127.0.0.1";
|
|
Logger.LogInformation("Attempting to connect to file server [{Arg0}]", (IPAddress == "127.0.0.1" ? "USB" : IPAddress));
|
|
if (!client.OpenConnection(IPAddress))
|
|
{
|
|
Logger.LogInformation("Not connected, attempting to start file server");
|
|
if (!client.StartServer(PackageName, AFSToken, IPAddress))
|
|
{
|
|
// try one more time with longer delay
|
|
Logger.LogInformation("Trying again");
|
|
Thread.Sleep(1000);
|
|
if (!client.StartServer(PackageName, AFSToken, IPAddress))
|
|
{
|
|
Logger.LogWarning("Failed to start Android file server for {PackageName}, skipping deploy for {DeviceName}", PackageName, DeviceName);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// verify we connected to the right server
|
|
string DevicePackageName = client.Query("^packagename");
|
|
if (DevicePackageName != PackageName)
|
|
{
|
|
Logger.LogInformation("Connected to wrong server {DevicePackageName}, trying again", DevicePackageName);
|
|
client.TerminateServer();
|
|
|
|
Logger.LogInformation("Trying to start file server {PackageName}", PackageName);
|
|
if (!client.StartServer(PackageName, AFSToken, IPAddress))
|
|
{
|
|
// try one more time with longer delay
|
|
Logger.LogInformation("Trying again");
|
|
Thread.Sleep(1000);
|
|
if (!client.StartServer(PackageName, AFSToken, IPAddress))
|
|
{
|
|
Logger.LogWarning("Failed to start Android file server for {PackageName}, skipping deploy for {DeviceName}", PackageName, DeviceName);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ConnectionType == EConnectionType.Combined)
|
|
{
|
|
IPAddress = (bUseManualIPAddress ? ManualIPAddress : client.Query("^ip"));
|
|
client2 = new AndroidFileClient(DeviceName);
|
|
|
|
Logger.LogInformation("Attempting to connect to file server [{IPAddress}]", IPAddress);
|
|
if (!client2.OpenConnection(IPAddress))
|
|
{
|
|
Logger.LogInformation("Not connected, attempting to start file server");
|
|
if (!client2.StartServer(PackageName, AFSToken, IPAddress, false))
|
|
{
|
|
Logger.LogWarning("Failed to start Android file server for {PackageName}, only using one connection for {DeviceName}", PackageName, DeviceName);
|
|
client2 = null;
|
|
}
|
|
}
|
|
|
|
// verify we connected to the right server
|
|
DevicePackageName = client2.Query("^packagename");
|
|
if (DevicePackageName != PackageName)
|
|
{
|
|
Logger.LogInformation("Connected to wrong server {DevicePackageName} for [{IPAddress}], not using network", DevicePackageName, IPAddress);
|
|
client2.CloseConnection();
|
|
client2 = null;
|
|
}
|
|
}
|
|
|
|
// get RemoteDir again (should be valid now after install / restart)
|
|
RemoteDir = client.Query("^project", true);
|
|
|
|
// write new timestamp for APK (do it here since RemoteDir now available)
|
|
if (bNeedAPKInstall)
|
|
{
|
|
client.FileWriteString("APK: " + APKLastUpdateTime + "\n", "^ext/APKFileStamp.txt");
|
|
}
|
|
|
|
// always update libUnreal.so
|
|
// TODO potential optimization not to push it every time but compare filestamp instead to check if we need to update it
|
|
if (bDontBundleLibrariesInAPK)
|
|
{
|
|
string FinalSONameStripped = Path.Combine(Path.GetDirectoryName(FinalSOName), Path.GetFileNameWithoutExtension(FinalSOName) + "-stripped" + Path.GetExtension(FinalSOName));
|
|
client.PushFile(FinalSONameStripped, "^int/libUnreal.so", true);
|
|
}
|
|
|
|
// update the uecommandline.txt
|
|
// update and deploy uecommandline.txt
|
|
// always delete the existing commandline text file, so it doesn't reuse an old one
|
|
FileReference IntermediateCmdLineFile = FileReference.Combine(SC.StageDirectory, "UECommandLine.txt");
|
|
Project.WriteStageCommandline(IntermediateCmdLineFile, Params, SC);
|
|
|
|
// copy files to device if we were staging
|
|
if (SC.Stage)
|
|
{
|
|
HashSet<string> EntriesToDeploy = new HashSet<string>();
|
|
|
|
// Fresh install always needs full deploy
|
|
if (Params.IterativeDeploy && !bFreshInstall)
|
|
{
|
|
// always send UECommandLine.txt (it was written above after delta checks applied)
|
|
EntriesToDeploy.Add(IntermediateCmdLineFile.FullName);
|
|
|
|
// Add non UFS files if any to deploy
|
|
String NonUFSManifestPath = SC.GetNonUFSDeploymentDeltaPath(DeviceName);
|
|
if (File.Exists(NonUFSManifestPath))
|
|
{
|
|
string NonUFSFiles = File.ReadAllText(NonUFSManifestPath);
|
|
foreach (string Filename in NonUFSFiles.Split('\n'))
|
|
{
|
|
if (!string.IsNullOrEmpty(Filename) && !string.IsNullOrWhiteSpace(Filename))
|
|
{
|
|
EntriesToDeploy.Add(CombinePaths(SC.StageDirectory.FullName, Filename.Trim()));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add UFS files if any to deploy
|
|
String UFSManifestPath = SC.GetUFSDeploymentDeltaPath(DeviceName);
|
|
if (File.Exists(UFSManifestPath))
|
|
{
|
|
string UFSFiles = File.ReadAllText(UFSManifestPath);
|
|
foreach (string Filename in UFSFiles.Split('\n'))
|
|
{
|
|
if (!string.IsNullOrEmpty(Filename) && !string.IsNullOrWhiteSpace(Filename))
|
|
{
|
|
EntriesToDeploy.Add(CombinePaths(SC.StageDirectory.FullName, Filename.Trim()));
|
|
}
|
|
}
|
|
}
|
|
|
|
// For now, if too many files may be better to just push them all
|
|
if (EntriesToDeploy.Count > 500)
|
|
{
|
|
// make sure device is at a clean state
|
|
client.DirDeleteRecurse(RemoteDir);
|
|
|
|
EntriesToDeploy.Clear();
|
|
EntriesToDeploy.TrimExcess();
|
|
EntriesToDeploy.Add(SC.StageDirectory.FullName);
|
|
}
|
|
else
|
|
{
|
|
// Discover & remove any files on device that are not in staging
|
|
|
|
// get listing of remote directory from device
|
|
string CommandResult = client.DirListFlat(RemoteDir);
|
|
|
|
if (CommandResult == null)
|
|
{
|
|
Logger.LogWarning("Failed to read remote dir: {RemoteDir}", RemoteDir);
|
|
RemoteDir = client.Query("^project", true);
|
|
CommandResult = client.DirListFlat(RemoteDir);
|
|
if (CommandResult == null)
|
|
{
|
|
Logger.LogWarning("Failed to read remote dir again: {RemoteDir}", RemoteDir);
|
|
}
|
|
}
|
|
|
|
{
|
|
// listing output is of the form
|
|
// [Samsung] [Google]
|
|
//
|
|
// RemoteDir/RestOfPath: RemoteDir/RestOfPath:
|
|
// - File1.png File1.png
|
|
// - File2.txt File2.txt
|
|
// d SubDir1 SubDir1/
|
|
// d SubDir2 Subdir2/
|
|
//
|
|
// RemoteDir/RestOfPath/SubDir1:
|
|
|
|
HashSet<string> DirsToDeleteFromDevice = new HashSet<string>();
|
|
List<string> FilesToDeleteFromDevice = new List<string>();
|
|
|
|
using (var reader = new StringReader(CommandResult))
|
|
{
|
|
string ProjectSaved = Params.ShortProjectName + "/Saved";
|
|
string ProjectConfig = Params.ShortProjectName + "/Config";
|
|
const string EngineSaved = "Engine/Saved"; // is this safe to use, or should we use SC.EngineRoot.GetDirectoryName()?
|
|
const string EngineConfig = "Engine/Config";
|
|
Logger.LogWarning("Excluding {ProjectSaved} {ProjectConfig} {EngineSaved} {EngineConfig} from clean during deployment.", ProjectSaved, ProjectConfig, EngineSaved, EngineConfig);
|
|
|
|
string CurrentDir = "";
|
|
bool SkipFiles = false;
|
|
for (string Line = reader.ReadLine(); Line != null; Line = reader.ReadLine())
|
|
{
|
|
if (String.IsNullOrWhiteSpace(Line))
|
|
{
|
|
continue; // ignore blank lines
|
|
}
|
|
|
|
if (Line.EndsWith(":"))
|
|
{
|
|
// RemoteDir/RestOfPath:
|
|
// keep ^--------^
|
|
CurrentDir = Line.Substring(RemoteDir.Length + 1, Math.Max(0, Line.Length - RemoteDir.Length - 2));
|
|
// Max is there for the case of base "RemoteDir:" --> ""
|
|
|
|
// We want to keep config & logs between deployments.
|
|
if (CurrentDir.StartsWith(ProjectSaved) || CurrentDir.StartsWith(ProjectConfig) || CurrentDir.StartsWith(EngineSaved) || CurrentDir.StartsWith(EngineConfig))
|
|
{
|
|
SkipFiles = true;
|
|
continue;
|
|
}
|
|
|
|
bool DirExistsInStagingArea = Directory.Exists(Path.Combine(SC.StageDirectory.FullName, CurrentDir));
|
|
if (DirExistsInStagingArea)
|
|
{
|
|
SkipFiles = false;
|
|
}
|
|
else
|
|
{
|
|
// delete directory from device
|
|
SkipFiles = true;
|
|
DirsToDeleteFromDevice.Add(CurrentDir);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (SkipFiles)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string FileName = GetFileNameFromListing(Line);
|
|
if (FileName != null)
|
|
{
|
|
bool FileExistsInStagingArea = File.Exists(Path.Combine(SC.StageDirectory.FullName, CurrentDir, FileName));
|
|
if (FileExistsInStagingArea)
|
|
{
|
|
// keep or overwrite
|
|
}
|
|
else if (FileName == "APKFileStamp.txt")
|
|
{
|
|
// keep it
|
|
}
|
|
else
|
|
{
|
|
// delete file from device
|
|
string FilePath = CurrentDir.Length == 0 ? FileName : (CurrentDir + "/" + FileName); // use / for Android target, no matter the development system
|
|
Logger.LogWarning("Deleting {FilePath} from device; not found in staging area", FilePath);
|
|
FilesToDeleteFromDevice.Add(FilePath);
|
|
}
|
|
}
|
|
// We ignore subdirs here as each will have its own "RemoteDir/CurrentDir/SubDir:" entry.
|
|
}
|
|
}
|
|
}
|
|
|
|
// delete directories
|
|
foreach (var DirToDelete in DirsToDeleteFromDevice)
|
|
{
|
|
// if a whole tree is to be deleted, don't spend extra commands deleting its branches
|
|
int FinalSlash = DirToDelete.LastIndexOf('/');
|
|
string ParentDir = FinalSlash >= 0 ? DirToDelete.Substring(0, FinalSlash) : "";
|
|
bool ParentMarkedForDeletion = DirsToDeleteFromDevice.Contains(ParentDir);
|
|
if (!ParentMarkedForDeletion)
|
|
{
|
|
Logger.LogWarning("Deleting {DirToDelete} and its contents from device; not found in staging area", DirToDelete);
|
|
client.DirDeleteRecurse(RemoteDir + "/" + DirToDelete);
|
|
}
|
|
}
|
|
|
|
// delete loose files
|
|
foreach (var FileToDelete in FilesToDeleteFromDevice)
|
|
{
|
|
client.FileDelete(RemoteDir + "/" + FileToDelete);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// make sure device is at a clean state
|
|
client.DirDeleteRecurse(RemoteDir);
|
|
|
|
// Copy UFS files..
|
|
string[] Files = Directory.GetFiles(SC.StageDirectory.FullName, "*", SearchOption.AllDirectories);
|
|
System.Array.Sort(Files);
|
|
|
|
// Find all the files we exclude from copying. And include
|
|
// the directories we need to individually copy.
|
|
HashSet<string> ExcludedFiles = new HashSet<string>();
|
|
SortedSet<string> IndividualCopyDirectories
|
|
= new SortedSet<string>((IComparer<string>)new LongestFirst());
|
|
foreach (string Filename in Files)
|
|
{
|
|
bool Exclude = false;
|
|
// Don't push the apk, we install it
|
|
Exclude |= Path.GetExtension(Filename).Equals(".apk", StringComparison.InvariantCultureIgnoreCase);
|
|
// For excluded files we add the parent dirs to our
|
|
// tracking of stuff to individually copy.
|
|
if (Exclude)
|
|
{
|
|
ExcludedFiles.Add(Filename);
|
|
// We include all directories up to the stage root in having
|
|
// to individually copy the files.
|
|
for (string FileDirectory = Path.GetDirectoryName(Filename);
|
|
!FileDirectory.Equals(SC.StageDirectory);
|
|
FileDirectory = Path.GetDirectoryName(FileDirectory))
|
|
{
|
|
if (!IndividualCopyDirectories.Contains(FileDirectory))
|
|
{
|
|
IndividualCopyDirectories.Add(FileDirectory);
|
|
}
|
|
}
|
|
if (!IndividualCopyDirectories.Contains(SC.StageDirectory.FullName))
|
|
{
|
|
IndividualCopyDirectories.Add(SC.StageDirectory.FullName);
|
|
}
|
|
}
|
|
}
|
|
|
|
// The directories are sorted above in "deepest" first. We can
|
|
// therefore start copying those individual dirs which will
|
|
// recreate the tree. As the subtrees will get copied at each
|
|
// possible individual level.
|
|
foreach (string DirectoryName in IndividualCopyDirectories)
|
|
{
|
|
string[] Entries
|
|
= Directory.GetFileSystemEntries(DirectoryName, "*", SearchOption.TopDirectoryOnly);
|
|
foreach (string Entry in Entries)
|
|
{
|
|
// We avoid excluded files and the individual copy dirs
|
|
// (the individual copy dirs will get handled as we iterate).
|
|
if (ExcludedFiles.Contains(Entry) || IndividualCopyDirectories.Contains(Entry))
|
|
{
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
EntriesToDeploy.Add(Entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (EntriesToDeploy.Count == 0)
|
|
{
|
|
EntriesToDeploy.Add(SC.StageDirectory.FullName);
|
|
}
|
|
}
|
|
|
|
// delete the .obb file, since it will cause nothing we deploy to be used
|
|
client.FileDelete(DeviceObbName);
|
|
client.FileDelete(DevicePatchName);
|
|
|
|
// delete existing overflow files on device, one more than the number we have to ensure we do not try to mount an old extra one left on device
|
|
for (int Index = 1; Index <= AllowOverflowOBBLimit; Index++)
|
|
{
|
|
string DeviceOverflowName = StorageLocation + "/" + GetDeviceOverflowName(ApkName, SC, Index);
|
|
client.FileDelete(DeviceOverflowName);
|
|
|
|
// stop if there is no staged overflow of this index
|
|
string OverflowPath = Path.Combine(SC.StageDirectory.FullName, GetFinalOverflowName(ApkName, SC, Index));
|
|
if (!File.Exists(OverflowPath))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We now have a minimal set of file & dir entries we need
|
|
// to deploy. Files we deploy will get individually copied
|
|
// and dirs will get the tree copies by default (that's
|
|
// what ADB does, too).
|
|
Logger.LogInformation("Deploying files using AFS");
|
|
string SourceDir = SC.StageDirectory.FullName;
|
|
client.Deploy(EntriesToDeploy, SourceDir, RemoteDir, bUseCompression, bLogFiles, bReportStats, client2);
|
|
}
|
|
else if (SC.Archive)
|
|
{
|
|
// deploy the obb if there is one
|
|
string ObbPath = Path.Combine(SC.StageDirectory.FullName, GetFinalObbName(ApkName, SC));
|
|
if (File.Exists(ObbPath))
|
|
{
|
|
client.FileWrite(ObbPath, DeviceObbName);
|
|
}
|
|
|
|
// deploy the patch if there is one
|
|
string PatchPath = Path.Combine(SC.StageDirectory.FullName, GetFinalPatchName(ApkName, SC));
|
|
if (File.Exists(PatchPath))
|
|
{
|
|
client.FileWrite(PatchPath, DevicePatchName);
|
|
}
|
|
|
|
for (int Index = 1; Index <= AllowOverflowOBBLimit; Index++)
|
|
{
|
|
string OverflowPath = Path.Combine(SC.StageDirectory.FullName, GetFinalOverflowName(ApkName, SC, Index));
|
|
if (File.Exists(OverflowPath))
|
|
{
|
|
string DeviceOverflowName = StorageLocation + "/" + GetDeviceOverflowName(ApkName, SC, Index);
|
|
client.FileWrite(OverflowPath, DeviceOverflowName);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// cache some strings
|
|
string RemoteFilename = IntermediateCmdLineFile.FullName.Replace(SC.StageDirectory.FullName, RemoteDir).Replace("\\", "/");
|
|
client.FileWrite(IntermediateCmdLineFile.FullName, RemoteFilename);
|
|
}
|
|
|
|
// terminate server and close AFS connections
|
|
if (client != null)
|
|
{
|
|
client.TerminateServer();
|
|
client.CloseConnection();
|
|
}
|
|
if (client2 != null)
|
|
{
|
|
client2.TerminateServer();
|
|
client2.CloseConnection();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DeployADB(ProjectParams Params, DeploymentContext SC)
|
|
{
|
|
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(Params.RawProjectPath), UnrealTargetPlatform.Android);
|
|
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bEnableMaliPerfCounters", out bool bDisablePerfHarden);
|
|
bool bDontBundleLibrariesInAPK = GetDontBundleLibrariesInAPK(Params, SC, true);
|
|
int AllowOverflowOBBLimit = AllowOverflowOBBFiles(SC);
|
|
|
|
foreach (string DeviceName in Params.DeviceNames.Select(DeviceName => GetSerialNumber(DeviceName)))
|
|
{
|
|
UnrealArch? DeviceArchitecture = GetBestDeviceArchitecture(Params, DeviceName);
|
|
string ApkName = GetFinalApkName(Params, SC.StageExecutables[0], true, DeviceArchitecture);
|
|
string FinalSOName = GetSOName(Params, SC.StageExecutables[0], DeviceArchitecture);
|
|
|
|
// make sure APK is up to date (this is fast if so)
|
|
var Deploy = AndroidExports.CreateDeploymentHandler(Params.RawProjectPath, Params.ForcePackageData);
|
|
if (!Params.Prebuilt)
|
|
{
|
|
string CookFlavor = SC.FinalCookPlatform.IndexOf("_") > 0 ? SC.FinalCookPlatform.Substring(SC.FinalCookPlatform.IndexOf("_")) : "";
|
|
string SOName = GetSONameWithoutArchitecture(Params, SC.StageExecutables[0]);
|
|
Deploy.SetAndroidPluginData(GetDeploymentArchitectures(Params, SC), CollectPluginDataPaths(SC));
|
|
Deploy.PrepForUATPackageOrDeploy(Params.RawProjectPath, Params.ShortProjectName, SC.ProjectRoot, SOName, SC.LocalRoot + "/Engine", Params.Distribution, CookFlavor, SC.StageTargets[0].Receipt.Configuration, true, false, SC.Archive);
|
|
}
|
|
|
|
// now we can use the apk to get more info
|
|
string PackageName = GetPackageInfo(ApkName, SC, false);
|
|
|
|
// Setup the OBB name and add the storage path (queried from the device) to it
|
|
string DeviceStorageQueryCommand = GetStorageQueryCommand();
|
|
IProcessResult Result = RunAdbCommand(Params, DeviceName, DeviceStorageQueryCommand, null, ERunOptions.AppMustExist);
|
|
String StorageLocation = Result.Output.Trim(); // "/mnt/sdcard";
|
|
string DeviceObbName = StorageLocation + "/" + GetDeviceObbName(ApkName, SC);
|
|
string DevicePatchName = StorageLocation + "/" + GetDevicePatchName(ApkName, SC);
|
|
string RemoteDir = StorageLocation + "/UnrealGame/" + Params.ShortProjectName;
|
|
|
|
if (bDisablePerfHarden)
|
|
{
|
|
RunAdbCommand(Params, DeviceName, "shell setprop security.perf_harden 0");
|
|
}
|
|
|
|
// remove any main or patch OBB file which would override deployed data (note: not the same as later delete or deploy)
|
|
{
|
|
string DeviceOldObbName = StorageLocation + "/Android/" + GetDeviceObbName(ApkName, SC);
|
|
string DeviceOldPatchName = StorageLocation + "/Android/" + GetDevicePatchName(ApkName, SC);
|
|
|
|
RunAdbCommand(Params, DeviceName, "shell rm " + DeviceOldObbName);
|
|
RunAdbCommand(Params, DeviceName, "shell rm " + DeviceOldPatchName);
|
|
}
|
|
|
|
// determine if APK out of date
|
|
string APKLastUpdateTime = new FileInfo(ApkName).LastWriteTime.ToString();
|
|
bool bNeedAPKInstall = true;
|
|
if (Params.IterativeDeploy)
|
|
{
|
|
// Check for apk installed with this package name on the device
|
|
IProcessResult InstalledResult = RunAdbCommand(Params, DeviceName, "shell pm list packages " + PackageName, null, ERunOptions.AppMustExist);
|
|
if (InstalledResult.Output.Contains(PackageName))
|
|
{
|
|
// See if apk is up to date on device
|
|
InstalledResult = RunAdbCommand(Params, DeviceName, "shell cat " + RemoteDir + "/APKFileStamp.txt", null, ERunOptions.AppMustExist);
|
|
if (InstalledResult.Output.StartsWith("APK: "))
|
|
{
|
|
if (InstalledResult.Output.Substring(5).Trim() == APKLastUpdateTime)
|
|
bNeedAPKInstall = false;
|
|
|
|
// Stop the previously running copy (uninstall/install did this before)
|
|
InstalledResult = RunAdbCommand(Params, DeviceName, "shell am force-stop " + PackageName, null, ERunOptions.AppMustExist);
|
|
if (InstalledResult.Output.Contains("Error"))
|
|
{
|
|
// force-stop not supported (Android < 3.0) so check if package is actually running
|
|
// Note: cannot use grep here since it may not be installed on device
|
|
InstalledResult = RunAdbCommand(Params, DeviceName, "shell ps", null, ERunOptions.AppMustExist);
|
|
if (InstalledResult.Output.Contains(PackageName))
|
|
{
|
|
// it is actually running so use the slow way to kill it (uninstall and reinstall)
|
|
bNeedAPKInstall = true;
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// install new APK if needed
|
|
if (bNeedAPKInstall)
|
|
{
|
|
// try uninstalling an old app with the same identifier.
|
|
int SuccessCode = 0;
|
|
string UninstallCommandline = "uninstall " + PackageName;
|
|
RunAndLogAdbCommand(Params, DeviceName, UninstallCommandline, out SuccessCode);
|
|
|
|
// install the apk
|
|
string InstallCommandline = "install \"" + ApkName + "\"";
|
|
string InstallOutput = RunAndLogAdbCommand(Params, DeviceName, InstallCommandline, out SuccessCode);
|
|
int FailureIndex = InstallOutput.IndexOf("Failure");
|
|
|
|
// adb install doesn't always return an error code on failure, and instead prints "Failure", followed by an error code.
|
|
if (SuccessCode != 0 || FailureIndex != -1)
|
|
{
|
|
string ErrorMessage = string.Format("Installation of apk '{0}' failed", ApkName);
|
|
if (FailureIndex != -1)
|
|
{
|
|
string FailureString = InstallOutput.Substring(FailureIndex + 7).Trim();
|
|
if (FailureString != "")
|
|
{
|
|
ErrorMessage += ": " + FailureString;
|
|
}
|
|
}
|
|
if (ErrorMessage.Contains("OLDER_SDK"))
|
|
{
|
|
Logger.LogError("minSdkVersion is higher than Android version installed on device, possibly due to NDK API Level");
|
|
}
|
|
throw new AutomationException(ExitCode.Error_AppInstallFailed, ErrorMessage);
|
|
}
|
|
else
|
|
{
|
|
// giving EXTERNAL_STORAGE_WRITE permission to the apk for API23+
|
|
// without this permission apk can't access to the assets put into the device
|
|
string ReadPermissionCommandLine = "shell pm grant " + PackageName + " android.permission.READ_EXTERNAL_STORAGE";
|
|
string WritePermissionCommandLine = "shell pm grant " + PackageName + " android.permission.WRITE_EXTERNAL_STORAGE";
|
|
RunAndLogAdbCommand(Params, DeviceName, ReadPermissionCommandLine, out SuccessCode);
|
|
RunAndLogAdbCommand(Params, DeviceName, WritePermissionCommandLine, out SuccessCode);
|
|
}
|
|
}
|
|
|
|
if (bDontBundleLibrariesInAPK)
|
|
{
|
|
int SuccessCode = 0;
|
|
|
|
string FinalSOPathStripped = Path.Combine(Path.GetDirectoryName(FinalSOName), Path.GetFileNameWithoutExtension(FinalSOName) + "-stripped" + Path.GetExtension(FinalSOName));
|
|
string FinalSOFileNameStripped = Path.GetFileName(FinalSOPathStripped);
|
|
|
|
string PushSO = $"push -z lz4 {FinalSOPathStripped} /data/local/tmp/{FinalSOFileNameStripped}";
|
|
string CopySO = $"shell run-as {PackageName} cp /data/local/tmp/{FinalSOFileNameStripped} ./files/libUnreal.so";
|
|
string DeleteSO = $"shell rm /data/local/tmp/{FinalSOFileNameStripped}";
|
|
RunAndLogAdbCommand(Params, DeviceName, PushSO, out SuccessCode);
|
|
RunAndLogAdbCommand(Params, DeviceName, CopySO, out SuccessCode);
|
|
RunAndLogAdbCommand(Params, DeviceName, DeleteSO, out SuccessCode);
|
|
|
|
if (SuccessCode != 0)
|
|
{
|
|
string ErrorMessage = $"Installation of '{PackageName}' failed due to failing to push libUnreal.so outside of {ApkName}";
|
|
throw new AutomationException(ExitCode.Error_AppInstallFailed, ErrorMessage);
|
|
}
|
|
}
|
|
|
|
// update the uecommandline.txt
|
|
// update and deploy uecommandline.txt
|
|
// always delete the existing commandline text file, so it doesn't reuse an old one
|
|
FileReference IntermediateCmdLineFile = FileReference.Combine(SC.StageDirectory, "UECommandLine.txt");
|
|
Project.WriteStageCommandline(IntermediateCmdLineFile, Params, SC);
|
|
|
|
// copy files to device if we were staging
|
|
if (SC.Stage)
|
|
{
|
|
// cache some strings
|
|
string BaseCommandline = "push";
|
|
|
|
HashSet<string> EntriesToDeploy = new HashSet<string>();
|
|
|
|
if (Params.IterativeDeploy)
|
|
{
|
|
// always send UECommandLine.txt (it was written above after delta checks applied)
|
|
EntriesToDeploy.Add(IntermediateCmdLineFile.FullName);
|
|
|
|
// Add non UFS files if any to deploy
|
|
String NonUFSManifestPath = SC.GetNonUFSDeploymentDeltaPath(DeviceName);
|
|
if (File.Exists(NonUFSManifestPath))
|
|
{
|
|
string NonUFSFiles = File.ReadAllText(NonUFSManifestPath);
|
|
foreach (string Filename in NonUFSFiles.Split('\n'))
|
|
{
|
|
if (!string.IsNullOrEmpty(Filename) && !string.IsNullOrWhiteSpace(Filename))
|
|
{
|
|
EntriesToDeploy.Add(CombinePaths(SC.StageDirectory.FullName, Filename.Trim()));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add UFS files if any to deploy
|
|
String UFSManifestPath = SC.GetUFSDeploymentDeltaPath(DeviceName);
|
|
if (File.Exists(UFSManifestPath))
|
|
{
|
|
string UFSFiles = File.ReadAllText(UFSManifestPath);
|
|
foreach (string Filename in UFSFiles.Split('\n'))
|
|
{
|
|
if (!string.IsNullOrEmpty(Filename) && !string.IsNullOrWhiteSpace(Filename))
|
|
{
|
|
EntriesToDeploy.Add(CombinePaths(SC.StageDirectory.FullName, Filename.Trim()));
|
|
}
|
|
}
|
|
}
|
|
|
|
// For now, if too many files may be better to just push them all
|
|
if (EntriesToDeploy.Count > 500)
|
|
{
|
|
// make sure device is at a clean state
|
|
RunAdbCommand(Params, DeviceName, "shell rm -r " + RemoteDir);
|
|
|
|
EntriesToDeploy.Clear();
|
|
EntriesToDeploy.TrimExcess();
|
|
EntriesToDeploy.Add(SC.StageDirectory.FullName);
|
|
}
|
|
else
|
|
{
|
|
// Discover & remove any files on device that are not in staging
|
|
|
|
// get listing of remote directory from device
|
|
string Commandline = "shell ls -RF1 " + RemoteDir;
|
|
var CommandResult = RunAdbCommand(Params, DeviceName, Commandline, null, ERunOptions.AppMustExist);
|
|
// CommandResult.ExitCode is adb shell's exit code, not ls exit code, which is what we need.
|
|
// Check output for error message instead.
|
|
if (CommandResult.Output.StartsWith("ls: "))
|
|
{
|
|
// list command failed, try simpler options
|
|
Commandline = "shell ls -RF " + RemoteDir;
|
|
CommandResult = RunAdbCommand(Params, DeviceName, Commandline, null, ERunOptions.AppMustExist);
|
|
}
|
|
|
|
if (CommandResult.Output.StartsWith("ls: "))
|
|
{
|
|
// list command failed, so clean the remote dir instead of selectively deleting files
|
|
RunAdbCommand(Params, DeviceName, "shell rm -r " + RemoteDir);
|
|
}
|
|
else
|
|
{
|
|
// listing output is of the form
|
|
// [Samsung] [Google]
|
|
//
|
|
// RemoteDir/RestOfPath: RemoteDir/RestOfPath:
|
|
// - File1.png File1.png
|
|
// - File2.txt File2.txt
|
|
// d SubDir1 SubDir1/
|
|
// d SubDir2 Subdir2/
|
|
//
|
|
// RemoteDir/RestOfPath/SubDir1:
|
|
|
|
HashSet<string> DirsToDeleteFromDevice = new HashSet<string>();
|
|
List<string> FilesToDeleteFromDevice = new List<string>();
|
|
|
|
using (var reader = new StringReader(CommandResult.Output))
|
|
{
|
|
string ProjectSaved = Params.ShortProjectName + "/Saved";
|
|
string ProjectConfig = Params.ShortProjectName + "/Config";
|
|
const string EngineSaved = "Engine/Saved"; // is this safe to use, or should we use SC.EngineRoot.GetDirectoryName()?
|
|
const string EngineConfig = "Engine/Config";
|
|
Logger.LogWarning("Excluding {ProjectSaved} {ProjectConfig} {EngineSaved} {EngineConfig} from clean during deployment.", ProjectSaved, ProjectConfig, EngineSaved, EngineConfig);
|
|
|
|
string CurrentDir = "";
|
|
bool SkipFiles = false;
|
|
for (string Line = reader.ReadLine(); Line != null; Line = reader.ReadLine())
|
|
{
|
|
if (String.IsNullOrWhiteSpace(Line))
|
|
{
|
|
continue; // ignore blank lines
|
|
}
|
|
|
|
if (Line.EndsWith(":"))
|
|
{
|
|
// RemoteDir/RestOfPath:
|
|
// keep ^--------^
|
|
CurrentDir = Line.Substring(RemoteDir.Length + 1, Math.Max(0, Line.Length - RemoteDir.Length - 2));
|
|
// Max is there for the case of base "RemoteDir:" --> ""
|
|
|
|
// We want to keep config & logs between deployments.
|
|
if (CurrentDir.StartsWith(ProjectSaved) || CurrentDir.StartsWith(ProjectConfig) || CurrentDir.StartsWith(EngineSaved) || CurrentDir.StartsWith(EngineConfig))
|
|
{
|
|
SkipFiles = true;
|
|
continue;
|
|
}
|
|
|
|
bool DirExistsInStagingArea = Directory.Exists(Path.Combine(SC.StageDirectory.FullName, CurrentDir));
|
|
if (DirExistsInStagingArea)
|
|
{
|
|
SkipFiles = false;
|
|
}
|
|
else
|
|
{
|
|
// delete directory from device
|
|
SkipFiles = true;
|
|
DirsToDeleteFromDevice.Add(CurrentDir);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (SkipFiles)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string FileName = GetFileNameFromListing(Line);
|
|
if (FileName != null)
|
|
{
|
|
bool FileExistsInStagingArea = File.Exists(Path.Combine(SC.StageDirectory.FullName, CurrentDir, FileName));
|
|
if (FileExistsInStagingArea)
|
|
{
|
|
// keep or overwrite
|
|
}
|
|
else if (FileName == "APKFileStamp.txt")
|
|
{
|
|
// keep it
|
|
}
|
|
else
|
|
{
|
|
// delete file from device
|
|
string FilePath = CurrentDir.Length == 0 ? FileName : (CurrentDir + "/" + FileName); // use / for Android target, no matter the development system
|
|
Logger.LogWarning("Deleting {FilePath} from device; not found in staging area", FilePath);
|
|
FilesToDeleteFromDevice.Add(FilePath);
|
|
}
|
|
}
|
|
// We ignore subdirs here as each will have its own "RemoteDir/CurrentDir/SubDir:" entry.
|
|
}
|
|
}
|
|
}
|
|
|
|
// delete directories
|
|
foreach (var DirToDelete in DirsToDeleteFromDevice)
|
|
{
|
|
// if a whole tree is to be deleted, don't spend extra commands deleting its branches
|
|
int FinalSlash = DirToDelete.LastIndexOf('/');
|
|
string ParentDir = FinalSlash >= 0 ? DirToDelete.Substring(0, FinalSlash) : "";
|
|
bool ParentMarkedForDeletion = DirsToDeleteFromDevice.Contains(ParentDir);
|
|
if (!ParentMarkedForDeletion)
|
|
{
|
|
Logger.LogWarning("Deleting {DirToDelete} and its contents from device; not found in staging area", DirToDelete);
|
|
RunAdbCommand(Params, DeviceName, "shell rm -r " + RemoteDir + "/" + DirToDelete);
|
|
}
|
|
}
|
|
|
|
// delete loose files
|
|
if (FilesToDeleteFromDevice.Count > 0)
|
|
{
|
|
// delete all stray files with one command
|
|
Commandline = String.Format("shell cd {0}; rm ", RemoteDir);
|
|
RunAdbCommand(Params, DeviceName, Commandline + String.Join(" ", FilesToDeleteFromDevice));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// make sure device is at a clean state
|
|
RunAdbCommand(Params, DeviceName, "shell rm -r " + RemoteDir);
|
|
|
|
// Copy UFS files..
|
|
string[] Files = Directory.GetFiles(SC.StageDirectory.FullName, "*", SearchOption.AllDirectories);
|
|
System.Array.Sort(Files);
|
|
|
|
// Find all the files we exclude from copying. And include
|
|
// the directories we need to individually copy.
|
|
HashSet<string> ExcludedFiles = new HashSet<string>();
|
|
SortedSet<string> IndividualCopyDirectories
|
|
= new SortedSet<string>((IComparer<string>)new LongestFirst());
|
|
foreach (string Filename in Files)
|
|
{
|
|
bool Exclude = false;
|
|
// Don't push the apk, we install it
|
|
Exclude |= Path.GetExtension(Filename).Equals(".apk", StringComparison.InvariantCultureIgnoreCase);
|
|
// For excluded files we add the parent dirs to our
|
|
// tracking of stuff to individually copy.
|
|
if (Exclude)
|
|
{
|
|
ExcludedFiles.Add(Filename);
|
|
// We include all directories up to the stage root in having
|
|
// to individually copy the files.
|
|
for (string FileDirectory = Path.GetDirectoryName(Filename);
|
|
!FileDirectory.Equals(SC.StageDirectory);
|
|
FileDirectory = Path.GetDirectoryName(FileDirectory))
|
|
{
|
|
if (!IndividualCopyDirectories.Contains(FileDirectory))
|
|
{
|
|
IndividualCopyDirectories.Add(FileDirectory);
|
|
}
|
|
}
|
|
if (!IndividualCopyDirectories.Contains(SC.StageDirectory.FullName))
|
|
{
|
|
IndividualCopyDirectories.Add(SC.StageDirectory.FullName);
|
|
}
|
|
}
|
|
}
|
|
|
|
// The directories are sorted above in "deepest" first. We can
|
|
// therefore start copying those individual dirs which will
|
|
// recreate the tree. As the subtrees will get copied at each
|
|
// possible individual level.
|
|
foreach (string DirectoryName in IndividualCopyDirectories)
|
|
{
|
|
string[] Entries
|
|
= Directory.GetFileSystemEntries(DirectoryName, "*", SearchOption.TopDirectoryOnly);
|
|
foreach (string Entry in Entries)
|
|
{
|
|
// We avoid excluded files and the individual copy dirs
|
|
// (the individual copy dirs will get handled as we iterate).
|
|
if (ExcludedFiles.Contains(Entry) || IndividualCopyDirectories.Contains(Entry))
|
|
{
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
EntriesToDeploy.Add(Entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (EntriesToDeploy.Count == 0)
|
|
{
|
|
EntriesToDeploy.Add(SC.StageDirectory.FullName);
|
|
}
|
|
}
|
|
|
|
// We now have a minimal set of file & dir entries we need
|
|
// to deploy. Files we deploy will get individually copied
|
|
// and dirs will get the tree copies by default (that's
|
|
// what ADB does).
|
|
HashSet<IProcessResult> DeployCommands = new HashSet<IProcessResult>();
|
|
foreach (string Entry in EntriesToDeploy)
|
|
{
|
|
string FinalRemoteDir = RemoteDir;
|
|
string RemotePath = Entry.Replace(SC.StageDirectory.FullName, FinalRemoteDir).Replace("\\", "/");
|
|
string Commandline = string.Format("{0} \"{1}\" \"{2}\"", BaseCommandline, Entry, RemotePath);
|
|
// We run deploy commands in parallel to maximize the connection
|
|
// throughput.
|
|
DeployCommands.Add(
|
|
RunAdbCommand(Params, DeviceName, Commandline, null,
|
|
ERunOptions.Default | ERunOptions.NoWaitForExit));
|
|
// But we limit the parallel commands to avoid overwhelming
|
|
// memory resources.
|
|
if (DeployCommands.Count == DeployMaxParallelCommands)
|
|
{
|
|
while (DeployCommands.Count > DeployMaxParallelCommands / 2)
|
|
{
|
|
Thread.Sleep(1);
|
|
DeployCommands.RemoveWhere(
|
|
delegate (IProcessResult r)
|
|
{
|
|
return r.HasExited;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
foreach (IProcessResult deploy_result in DeployCommands)
|
|
{
|
|
deploy_result.WaitForExit();
|
|
}
|
|
|
|
// delete the .obb file, since it will cause nothing we just deployed to be used
|
|
RunAdbCommand(Params, DeviceName, "shell rm " + DeviceObbName);
|
|
RunAdbCommand(Params, DeviceName, "shell rm " + DevicePatchName);
|
|
|
|
// delete existing overflow files on device, one more than the number we have to ensure we do not try to mount an old extra one left on device
|
|
for (int Index = 1; Index <= AllowOverflowOBBLimit; Index++)
|
|
{
|
|
string DeviceOverflowName = StorageLocation + "/" + GetDeviceOverflowName(ApkName, SC, Index);
|
|
RunAdbCommand(Params, DeviceName, "shell rm " + DeviceOverflowName);
|
|
|
|
string OverflowPath = Path.Combine(SC.StageDirectory.FullName, GetFinalOverflowName(ApkName, SC, Index));
|
|
if (!File.Exists(OverflowPath))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (SC.Archive)
|
|
{
|
|
// deploy the obb if there is one
|
|
string ObbPath = Path.Combine(SC.StageDirectory.FullName, GetFinalObbName(ApkName, SC));
|
|
if (File.Exists(ObbPath))
|
|
{
|
|
// cache some strings
|
|
string BaseCommandline = "push";
|
|
|
|
string Commandline = string.Format("{0} \"{1}\" \"{2}\"", BaseCommandline, ObbPath, DeviceObbName);
|
|
RunAdbCommand(Params, DeviceName, Commandline);
|
|
}
|
|
|
|
// deploy the patch if there is one
|
|
string PatchPath = Path.Combine(SC.StageDirectory.FullName, GetFinalPatchName(ApkName, SC));
|
|
if (File.Exists(PatchPath))
|
|
{
|
|
// cache some strings
|
|
string BaseCommandline = "push";
|
|
|
|
string Commandline = string.Format("{0} \"{1}\" \"{2}\"", BaseCommandline, PatchPath, DevicePatchName);
|
|
RunAdbCommand(Params, DeviceName, Commandline);
|
|
}
|
|
|
|
// deploy the overflows
|
|
for (int Index = 1; Index <= AllowOverflowOBBLimit; Index++)
|
|
{
|
|
string OverflowPath = Path.Combine(SC.StageDirectory.FullName, GetFinalOverflowName(ApkName, SC, Index));
|
|
if (File.Exists(OverflowPath))
|
|
{
|
|
// cache some strings
|
|
string BaseCommandline = "push";
|
|
string DeviceOverflowName = StorageLocation + "/" + GetDeviceOverflowName(ApkName, SC, Index);
|
|
|
|
string Commandline = string.Format("{0} \"{1}\" \"{2}\"", BaseCommandline, OverflowPath, DeviceOverflowName);
|
|
RunAdbCommand(Params, DeviceName, Commandline);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// cache some strings
|
|
string BaseCommandline = "push";
|
|
|
|
string FinalRemoteDir = RemoteDir;
|
|
/*
|
|
// handle the special case of the UECommandline.txt when using content only game (UnrealGame)
|
|
if (!Params.IsCodeBasedProject)
|
|
{
|
|
FinalRemoteDir = "/mnt/sdcard/UnrealGame";
|
|
}
|
|
*/
|
|
|
|
string RemoteFilename = IntermediateCmdLineFile.FullName.Replace(SC.StageDirectory.FullName, FinalRemoteDir).Replace("\\", "/");
|
|
string Commandline = string.Format("{0} \"{1}\" \"{2}\"", BaseCommandline, IntermediateCmdLineFile, RemoteFilename);
|
|
RunAdbCommand(Params, DeviceName, Commandline);
|
|
}
|
|
|
|
// write new timestamp for APK (do it here since RemoteDir will now exist)
|
|
if (bNeedAPKInstall)
|
|
{
|
|
int SuccessCode = 0;
|
|
RunAndLogAdbCommand(Params, DeviceName, "shell \"echo 'APK: " + APKLastUpdateTime + "' > " + RemoteDir + "/APKFileStamp.txt\"", out SuccessCode);
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void Deploy(ProjectParams Params, DeploymentContext SC)
|
|
{
|
|
bool bAFSEnablePlugin;
|
|
string AFSToken;
|
|
bool bIsShipping;
|
|
bool bAFSIncludeInShipping;
|
|
bool bAFSAllowExternalStartInShipping;
|
|
|
|
// Pick the proper deploy method
|
|
if (UsingAndroidFileServer(Params, SC, out bAFSEnablePlugin, out AFSToken, out bIsShipping, out bAFSIncludeInShipping, out bAFSAllowExternalStartInShipping))
|
|
{
|
|
DeployAndroidFileServer(Params, SC, AFSToken);
|
|
}
|
|
else
|
|
{
|
|
DeployADB(Params, SC);
|
|
}
|
|
}
|
|
|
|
/** Internal usage for GetPackageName */
|
|
private static string PackageLine = null;
|
|
private static Mutex PackageInfoMutex = new Mutex();
|
|
private static string LaunchableActivityLine = null;
|
|
private static string MetaAppTypeLine = null;
|
|
private static Dictionary<string,string> MetaDataMap = null;
|
|
|
|
/** Run an external exe (and capture the output), given the exe path and the commandline. */
|
|
public static string GetPackageInfo(string ApkName, bool bRetrieveVersionCode)
|
|
{
|
|
string ReturnValue = null;
|
|
|
|
// we expect there to be one, so use the first one
|
|
string AaptPath = GetAaptPath();
|
|
|
|
PackageInfoMutex.WaitOne();
|
|
|
|
if (File.Exists(ApkName))
|
|
{
|
|
var ExeInfo = new ProcessStartInfo(AaptPath, "dump --include-meta-data badging \"" + ApkName + "\"");
|
|
ExeInfo.UseShellExecute = false;
|
|
ExeInfo.RedirectStandardOutput = true;
|
|
using (var GameProcess = Process.Start(ExeInfo))
|
|
{
|
|
PackageLine = null;
|
|
LaunchableActivityLine = null;
|
|
MetaAppTypeLine = null;
|
|
MetaDataMap = null;
|
|
GameProcess.BeginOutputReadLine();
|
|
GameProcess.OutputDataReceived += ParsePackageName;
|
|
GameProcess.WaitForExit();
|
|
}
|
|
|
|
PackageInfoMutex.ReleaseMutex();
|
|
|
|
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];
|
|
}
|
|
}
|
|
Logger.LogInformation("GetPackageInfo ReturnValue: {ReturnValue}", ReturnValue);
|
|
}
|
|
|
|
return ReturnValue;
|
|
}
|
|
|
|
public static string GetPackageInfo(string ApkName, DeploymentContext SC, bool bRetrieveVersionCode)
|
|
{
|
|
string ReturnValue = GetPackageInfo(ApkName, bRetrieveVersionCode);
|
|
|
|
if (ReturnValue == null || ReturnValue.Length == 0)
|
|
{
|
|
// If APK does not exist or we cant find package info in apk use the packageInfo file
|
|
ReturnValue = GetPackageInfoFromInfoFile(ApkName, SC, bRetrieveVersionCode);
|
|
}
|
|
|
|
return ReturnValue;
|
|
}
|
|
|
|
/** Lookup package info in packageInfo.txt file in same directory as the APK would have been */
|
|
private static string GetPackageInfoFromInfoFile(string ApkName, DeploymentContext SC, bool bRetrieveVersionCode)
|
|
{
|
|
string ReturnValue = null;
|
|
String PackageInfoPath = Path.Combine(Path.GetDirectoryName(ApkName), "packageInfo.txt");
|
|
Boolean fileExists = File.Exists(PackageInfoPath);
|
|
if (fileExists)
|
|
{
|
|
string[] Lines = File.ReadAllLines(PackageInfoPath);
|
|
int LineIndex = bRetrieveVersionCode ? 1 : 0;
|
|
Logger.LogInformation("packageInfo line index: {LineIndex}", LineIndex);
|
|
if (Lines.Length >= 2)
|
|
{
|
|
ReturnValue = Lines[LineIndex];
|
|
}
|
|
// parse extra info that the aapt-based method got
|
|
MetaAppTypeLine = Lines[3];
|
|
}
|
|
|
|
if (bRetrieveVersionCode)
|
|
{
|
|
int StoreVersion = 1;
|
|
int.TryParse(ReturnValue, out StoreVersion);
|
|
|
|
int StoreVersionOffset = 0;
|
|
ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(SC.RawProjectPath), SC.StageTargetPlatform.PlatformType);
|
|
if (ApkName.Contains("-arm64-"))
|
|
{
|
|
Ini.GetInt32("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "StoreVersionOffsetArm64", out StoreVersionOffset);
|
|
}
|
|
else if (ApkName.Contains("-x64-"))
|
|
{
|
|
Ini.GetInt32("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "StoreVersionOffsetX8664", out StoreVersionOffset);
|
|
}
|
|
StoreVersion += StoreVersionOffset;
|
|
ReturnValue = StoreVersion.ToString("0");
|
|
}
|
|
|
|
Logger.LogInformation("packageInfo.txt file exists: {fileExists}", fileExists);
|
|
Logger.LogInformation("packageInfo return MetaAppTypeLine: {MetaAppTypeLine}", MetaAppTypeLine);
|
|
Logger.LogInformation("packageInfo return value: {ReturnValue}", ReturnValue);
|
|
|
|
return ReturnValue;
|
|
}
|
|
|
|
/** Returns the launch activity name to launch (must call GetPackageInfo first), returns "com.epicgames.unreal.SplashActivity" default if not found */
|
|
public static string GetLaunchableActivityName()
|
|
{
|
|
string ReturnValue = DefaultLaunchActivity;
|
|
if (LaunchableActivityLine != null)
|
|
{
|
|
// the line should look like: launchable-activity: name='com.epicgames.unreal.SplashActivity' label='TappyChicken' icon=''
|
|
string[] Tokens = LaunchableActivityLine.Split("'".ToCharArray());
|
|
if (Tokens.Length >= 2)
|
|
{
|
|
ReturnValue = Tokens[1];
|
|
}
|
|
}
|
|
return ReturnValue;
|
|
}
|
|
|
|
public static string GetLaunchableActivityName(string ApkName)
|
|
{
|
|
if (!string.IsNullOrEmpty(ApkName) && LaunchableActivityLine == null)
|
|
{
|
|
GetPackageInfo(ApkName, false);
|
|
}
|
|
return GetLaunchableActivityName();
|
|
}
|
|
|
|
/** Returns the app type from the packaged APK metadata, returns "" if not found */
|
|
public static string GetMetaAppType()
|
|
{
|
|
string ReturnValue = "";
|
|
if (MetaAppTypeLine != null)
|
|
{
|
|
// the line should look like: meta-data: name='com.epicgames.unreal.GameActivity.AppType' value='Client'
|
|
string[] Tokens = MetaAppTypeLine.Split("'".ToCharArray());
|
|
if (Tokens.Length >= 4)
|
|
{
|
|
ReturnValue = Tokens[3];
|
|
}
|
|
}
|
|
return ReturnValue;
|
|
}
|
|
|
|
public static string GetMetadataValue(string MetadataKey)
|
|
{
|
|
if(MetaDataMap != null)
|
|
{
|
|
string MetadataValue;
|
|
if ( MetaDataMap.TryGetValue(MetadataKey, out MetadataValue) )
|
|
{
|
|
return MetadataValue;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 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))
|
|
{
|
|
string Line = Event.Data;
|
|
if (PackageLine == null)
|
|
{
|
|
if (Line.StartsWith("package:"))
|
|
{
|
|
PackageLine = Line;
|
|
}
|
|
}
|
|
if (LaunchableActivityLine == null)
|
|
{
|
|
if (Line.StartsWith("launchable-activity:"))
|
|
{
|
|
LaunchableActivityLine = Line;
|
|
}
|
|
}
|
|
if (MetaAppTypeLine == null)
|
|
{
|
|
if (Line.StartsWith("meta-data: name='com.epicgames.unreal.GameActivity.AppType'"))
|
|
{
|
|
MetaAppTypeLine = Line;
|
|
}
|
|
}
|
|
if(Line.StartsWith("meta-data: name='com.epicgames.unreal.GameActivity"))
|
|
{
|
|
// We expect the meta-data string to be in the format of " meta-data: name='...' value='...' "
|
|
Match MetaDataMatch = Regex.Match(Line, @"meta-data: name='com.epicgames.unreal.GameActivity.(.*?)'.*?'(.*?)'");
|
|
if(MetaDataMatch.Groups.Count == 3)
|
|
{
|
|
if (MetaDataMap == null)
|
|
{
|
|
MetaDataMap = new Dictionary<string, string>();
|
|
}
|
|
try
|
|
{
|
|
MetaDataMap.Add(MetaDataMatch.Groups[1].Value, MetaDataMatch.Groups[2].Value);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogWarning("{Text}", @"Ignoring duplicate package metadata entry '"+Line+"\'.\n" + ex.ToString());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning("{Text}", "Unexpected layout of package metadata: " + Line);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static private string CachedAaptPath = null;
|
|
static private string LastAndroidHomePath = null;
|
|
|
|
private static uint GetRevisionValue(string VersionString)
|
|
{
|
|
// read up to 4 sections (ie. 20.0.3.5), first section most significant
|
|
// each section assumed to be 0 to 255 range
|
|
uint Value = 0;
|
|
try
|
|
{
|
|
string[] Sections= VersionString.Split(".".ToCharArray());
|
|
Value |= (Sections.Length > 0) ? (uint.Parse(Sections[0]) << 24) : 0;
|
|
Value |= (Sections.Length > 1) ? (uint.Parse(Sections[1]) << 16) : 0;
|
|
Value |= (Sections.Length > 2) ? (uint.Parse(Sections[2]) << 8) : 0;
|
|
Value |= (Sections.Length > 3) ? uint.Parse(Sections[3]) : 0;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// ignore poorly formed version
|
|
}
|
|
return Value;
|
|
}
|
|
|
|
private static string GetAaptPath()
|
|
{
|
|
// return cached path if ANDROID_HOME has not changed
|
|
string HomePath = Environment.ExpandEnvironmentVariables("%ANDROID_HOME%");
|
|
if (CachedAaptPath != null && LastAndroidHomePath == HomePath)
|
|
{
|
|
return CachedAaptPath;
|
|
}
|
|
|
|
// get a list of the directories in build-tools.. may be more than one set installed (or none which is bad)
|
|
string[] Subdirs = Directory.GetDirectories(Path.Combine(HomePath, "build-tools"));
|
|
if (Subdirs.Length == 0)
|
|
{
|
|
throw new AutomationException(ExitCode.Error_AndroidBuildToolsPathNotFound, "Failed to find %ANDROID_HOME%/build-tools subdirectory. Run SDK manager and install build-tools.");
|
|
}
|
|
|
|
// valid directories will have a source.properties with the Pkg.Revision (there is no guarantee we can use the directory name as revision)
|
|
string BestToolPath = null;
|
|
uint BestVersion = 0;
|
|
foreach (string CandidateDir in Subdirs)
|
|
{
|
|
string AaptFilename = Path.Combine(CandidateDir, OperatingSystem.IsWindows() ? "aapt.exe" : "aapt");
|
|
uint RevisionValue = 0;
|
|
|
|
if (File.Exists(AaptFilename))
|
|
{
|
|
string SourcePropFilename = Path.Combine(CandidateDir, "source.properties");
|
|
if (File.Exists(SourcePropFilename))
|
|
{
|
|
string[] PropertyContents = File.ReadAllLines(SourcePropFilename);
|
|
foreach (string PropertyLine in PropertyContents)
|
|
{
|
|
if (PropertyLine.StartsWith("Pkg.Revision="))
|
|
{
|
|
RevisionValue = GetRevisionValue(PropertyLine.Substring(13));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// remember it if newer version or haven't found one yet
|
|
if (RevisionValue > BestVersion || BestToolPath == null)
|
|
{
|
|
BestVersion = RevisionValue;
|
|
BestToolPath = AaptFilename;
|
|
}
|
|
}
|
|
|
|
if (BestToolPath == null)
|
|
{
|
|
throw new AutomationException(ExitCode.Error_AndroidBuildToolsPathNotFound, "Failed to find %ANDROID_HOME%/build-tools subdirectory with aapt. Run SDK manager and install build-tools.");
|
|
}
|
|
|
|
CachedAaptPath = BestToolPath;
|
|
LastAndroidHomePath = HomePath;
|
|
|
|
Logger.LogInformation("Using this aapt: {CachedAaptPath}", CachedAaptPath);
|
|
|
|
return CachedAaptPath;
|
|
}
|
|
|
|
static string GetIniValue(string Path, string Key)
|
|
{
|
|
try
|
|
{
|
|
using StreamReader Reader = File.OpenText(Path);
|
|
|
|
for (;;)
|
|
{
|
|
string Line = Reader.ReadLine();
|
|
|
|
if (Line == null)
|
|
{
|
|
break;
|
|
}
|
|
|
|
string[] Parts = Line.Split('=', 2);
|
|
|
|
if (Parts[0].Trim() == Key)
|
|
{
|
|
return Parts[1].Trim();
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static string GetSerialNumber(string DeviceName)
|
|
{
|
|
if (DeviceName.StartsWith("avd-"))
|
|
{
|
|
string AvdName = DeviceName[4..];
|
|
|
|
(string, string) QuerySerialNumber()
|
|
{
|
|
return RunAdbCommand(null, "devices", null, ERunOptions.AppMustExist).Output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).Skip(1).Where(Line => Line.StartsWith("emulator-")).Select(Line => Line.Split('\t', 2)).Where(Parts =>
|
|
{
|
|
IProcessResult Result = RunAdbCommand(Parts[0], "emu avd name", null, ERunOptions.AppMustExist);
|
|
return Result.bExitCodeSuccess && Result.Output[0..Result.Output.IndexOf(Environment.NewLine, StringComparison.Ordinal)] == AvdName;
|
|
}).Select(Parts => (Parts[0], Parts[1])).FirstOrDefault();
|
|
}
|
|
|
|
(string SerialNumber, string Status) = QuerySerialNumber();
|
|
|
|
if (SerialNumber == null)
|
|
{
|
|
using Process Process = Process.Start(new ProcessStartInfo(Environment.ExpandEnvironmentVariables("%ANDROID_HOME%/emulator/emulator"), ["-avd", AvdName]) { UseShellExecute = false, CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden })!;
|
|
|
|
for (uint RetryCount = 60;; --RetryCount)
|
|
{
|
|
bool bExited = Process.WaitForExit(1000);
|
|
|
|
(SerialNumber, Status) = QuerySerialNumber();
|
|
|
|
if (bExited || SerialNumber != null || RetryCount == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
string QueryStatus()
|
|
{
|
|
return RunAdbCommand(null, "devices", null, ERunOptions.AppMustExist).Output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).Skip(1).Select(Line => Line.Split('\t', 2)).Where(Parts => Parts[0] == SerialNumber).Select(Parts => Parts[1]).FirstOrDefault();
|
|
}
|
|
|
|
for (uint RetryCount = 60; Status != "device"; --RetryCount)
|
|
{
|
|
Logger.LogInformation("Device status for '{DeviceName}': {Status}", DeviceName, Status);
|
|
|
|
if (string.IsNullOrEmpty(Status) || RetryCount == 0)
|
|
{
|
|
throw new AutomationException(ExitCode.Error_UnknownLaunchFailure, "Unable to run because couldn't connect to device '{0}'", DeviceName);
|
|
}
|
|
|
|
Thread.Sleep(1000);
|
|
|
|
Status = QueryStatus();
|
|
}
|
|
|
|
return SerialNumber;
|
|
}
|
|
else
|
|
{
|
|
return DeviceName;
|
|
}
|
|
}
|
|
|
|
private UnrealArch? GetBestDeviceArchitecture(ProjectParams Params, string DeviceName)
|
|
{
|
|
bool bMakeSeparateApks = UnrealBuildTool.AndroidExports.ShouldMakeSeparateApks();
|
|
// if we are joining all .so's into a single .apk, there's no need to find the best one - there is no other one
|
|
if (!bMakeSeparateApks)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// @todo get TargetName?
|
|
UnrealArchitectures AppArchitectures = Params.ClientArchitecture ?? UnrealArchitectureConfig.ForPlatform(UnrealTargetPlatform.Android).ActiveArchitectures(Params.RawProjectPath, null);
|
|
|
|
// ask the device
|
|
IProcessResult ABIResult = RunAdbCommand(Params, DeviceName, " shell getprop ro.product.cpu.abi", null, ERunOptions.AppMustExist);
|
|
|
|
// the output is just the architecture
|
|
UnrealArch DeviceArch = UnrealBuildTool.AndroidExports.GetUnrealArch(ABIResult.Output.Trim());
|
|
|
|
// if the architecture wasn't built, look for a backup
|
|
if (!AppArchitectures.Contains(DeviceArch))
|
|
{
|
|
// Houdini emulation can run arm64 on intel
|
|
if (DeviceArch == UnrealArch.X64)
|
|
{
|
|
DeviceArch = UnrealArch.Arm64;
|
|
}
|
|
}
|
|
|
|
// if after the fallbacks, we still don't have it, we can't continue
|
|
if (!AppArchitectures.Contains(DeviceArch))
|
|
{
|
|
throw new AutomationException(ExitCode.Error_NoApkSuitableForArchitecture, "Unable to run because you don't have an apk that is usable on {0}. Looked for {1}", DeviceName, DeviceArch);
|
|
}
|
|
|
|
return DeviceArch;
|
|
}
|
|
|
|
private bool DeployClientCmdLineAFS(ProjectParams Params, string DeviceName, string PackageName, string AFSToken, string ClientCmdLine)
|
|
{
|
|
AndroidFileClient client = new AndroidFileClient(DeviceName);
|
|
if (!client.OpenConnection())
|
|
{
|
|
Logger.LogInformation("DeployClientCmdLine: Trying to start file server {PackageName}", PackageName);
|
|
if (!client.StartServer(PackageName, AFSToken))
|
|
{
|
|
Logger.LogInformation("DeployClientCmdLine: Failed to start server {PackageName}, ignoring client command line", PackageName);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// verify connection to the correct server
|
|
string DevicePackageName = client.Query("^packagename");
|
|
if (DevicePackageName != PackageName)
|
|
{
|
|
Logger.LogInformation("DeployClientCmdLine: Connected to wrong server {DevicePackageName}, trying again", DevicePackageName);
|
|
client.TerminateServer();
|
|
|
|
Logger.LogInformation("DeployClientCmdLine: Trying to start file server {PackageName}", PackageName);
|
|
if (!client.StartServer(PackageName, AFSToken))
|
|
{
|
|
Logger.LogInformation("DeployClientCmdLine: Failed to start server {PackageName}, ignoring client command line", PackageName);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Logger.LogInformation("Writing ClientCmdLine to remote ^commandfile: {ClientCmdLine}", ClientCmdLine);
|
|
client.FileWriteString(ClientCmdLine, "^commandfile");
|
|
client.TerminateServer();
|
|
client.CloseConnection();
|
|
return true;
|
|
}
|
|
|
|
private bool DeployClientCmdLineADB(ProjectParams Params, string DeviceName, string PackageName, string ClientCmdLine)
|
|
{
|
|
string DeviceStorageQueryCommand = GetStorageQueryCommand();
|
|
IProcessResult StorageResult = RunAdbCommand(Params, DeviceName, DeviceStorageQueryCommand, null, ERunOptions.AppMustExist);
|
|
string StorageLocation = StorageResult.Output.Trim();
|
|
string RemoteDir = StorageLocation + "/UnrealGame/" + Params.ShortProjectName;
|
|
string ClientCmdLineTmpFile = Path.GetTempFileName();
|
|
string ClientCmdLineRemoteFile = RemoteDir + "/UECommandLine.txt";
|
|
File.WriteAllText(ClientCmdLineTmpFile, ClientCmdLine);
|
|
Logger.LogInformation("Pushing ClientCmdLine to remote file {ClientCmdLineRemoteFile}: {ClientCmdLine}", ClientCmdLineRemoteFile, ClientCmdLine);
|
|
RunAdbCommand(DeviceName, String.Format("push {0} {1}", ClientCmdLineTmpFile, ClientCmdLineRemoteFile));
|
|
File.Delete(ClientCmdLineTmpFile);
|
|
return true;
|
|
}
|
|
|
|
public override void ModifyFileHostAddresses(List<string> HostAddresses)
|
|
{
|
|
HostAddresses.Insert(0, "127.0.0.1");
|
|
}
|
|
|
|
public override IProcessResult RunClient(ERunOptions ClientRunFlags, string ClientApp, string ClientCmdLine, ProjectParams Params)
|
|
{
|
|
IProcessResult Result = null;
|
|
|
|
string LogPath = Path.Combine(Params.BaseStageDirectory, "Android\\logs");
|
|
Directory.CreateDirectory(LogPath);
|
|
|
|
foreach (string DeviceName in Params.DeviceNames.Select(DeviceName => GetSerialNumber(DeviceName)))
|
|
{
|
|
//get the package name and save that
|
|
UnrealArch? DeviceArchitecture = GetBestDeviceArchitecture(Params, DeviceName);
|
|
|
|
//strip off the device, GPU architecture and extension (.so)
|
|
int DashIndex = ClientApp.LastIndexOf("-");
|
|
if (DashIndex >= 0)
|
|
{
|
|
ClientApp = ClientApp.Substring(0, DashIndex);
|
|
}
|
|
|
|
string ApkName = GetFinalApkName(Params, Path.GetFileNameWithoutExtension(ClientApp), true, DeviceArchitecture);
|
|
|
|
if (!File.Exists(ApkName))
|
|
{
|
|
throw new AutomationException(ExitCode.Error_AppNotFound, "Failed to find application " + ApkName);
|
|
}
|
|
|
|
// run aapt to get the name of the intent
|
|
string PackageName = GetPackageInfo(ApkName, false);
|
|
if (PackageName == null)
|
|
{
|
|
throw new AutomationException(ExitCode.Error_FailureGettingPackageInfo, "Failed to get package name from " + ClientApp);
|
|
}
|
|
|
|
var canReadClientCmdLineViaAmStart = Params.ClientConfigsToBuild.Count > 0 &&
|
|
Params.ClientConfigsToBuild.First() != UnrealTargetConfiguration.Shipping;
|
|
|
|
// push ClientCmdLine args as a file to the device to override the stage/apk command line if we can't push it via am start
|
|
if (!canReadClientCmdLineViaAmStart)
|
|
{
|
|
bool bAFSEnablePlugin;
|
|
string AFSToken;
|
|
bool bIsShipping;
|
|
bool bAFSIncludeInShipping;
|
|
bool bAFSAllowExternalStartInShipping;
|
|
if (UsingAndroidFileServer(Params, null, out bAFSEnablePlugin, out AFSToken, out bIsShipping, out bAFSIncludeInShipping, out bAFSAllowExternalStartInShipping))
|
|
{
|
|
DeployClientCmdLineAFS(Params, DeviceName, PackageName, AFSToken, ClientCmdLine);
|
|
}
|
|
else
|
|
{
|
|
DeployClientCmdLineADB(Params, DeviceName, PackageName, ClientCmdLine);
|
|
}
|
|
}
|
|
|
|
// Message back to the Unreal Editor to correctly set the app id for each device
|
|
Logger.LogInformation("Running Package@Device:{PackageName}@{DeviceName}", PackageName, DeviceName);
|
|
|
|
// clear the log for the device
|
|
RunAdbCommand(DeviceName, "logcat -c");
|
|
|
|
// start the app on device!
|
|
var CommandLine = "shell am start -n " + PackageName + "/" + GetLaunchableActivityName();
|
|
if (canReadClientCmdLineViaAmStart)
|
|
{
|
|
var ToScapeChars = new string[]{" ", "(", ")", "`", "$", "%", "&"};
|
|
|
|
var ClientSessionCmdLineEscaped = ClientCmdLine.Replace("\"", "\\\\\\\"");
|
|
foreach( var ToScape in ToScapeChars )
|
|
{
|
|
ClientSessionCmdLineEscaped = ClientSessionCmdLineEscaped.Replace(ToScape, "\\" + ToScape);
|
|
}
|
|
CommandLine += " --es cmdline \"" + ClientSessionCmdLineEscaped + "\"";
|
|
}
|
|
RunAdbCommand(DeviceName, CommandLine);
|
|
|
|
// wait before getting the process list with "adb shell ps" from AdbCreatedProcess
|
|
// on some devices the list is not yet ready
|
|
Thread.Sleep(2000);
|
|
|
|
// Start logging process and return immediately.
|
|
// Stdout from the title is continuosly emitted to stdout in UAT.
|
|
// When process is done the AdbCreatedProcess wrapper will save the output to the log directories
|
|
Result = RunAdbCommand(DeviceName, "logcat -s UE debug Debug DEBUG", null, ClientRunFlags | ERunOptions.NoWaitForExit);
|
|
Result = new AdbCreatedProcess(Result, LogPath, PackageName, DeviceName);
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
public override void GetFilesToDeployOrStage(ProjectParams Params, DeploymentContext SC)
|
|
{
|
|
// Add any Android shader cache files
|
|
DirectoryReference ProjectShaderDir = DirectoryReference.Combine(Params.RawProjectPath.Directory, "Build", "ShaderCaches", "Android");
|
|
if(DirectoryReference.Exists(ProjectShaderDir))
|
|
{
|
|
SC.StageFiles(StagedFileType.UFS, ProjectShaderDir, StageFilesSearch.AllDirectories);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets cook platform name for this platform.
|
|
/// </summary>
|
|
/// <returns>Cook platform string.</returns>
|
|
public override string GetCookPlatform(bool bDedicatedServer, bool bIsClientOnly)
|
|
{
|
|
return bIsClientOnly ? "AndroidClient" : "Android";
|
|
}
|
|
|
|
public override bool DeployLowerCaseFilenames(StagedFileType FileType)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public override string LocalPathToTargetPath(string LocalPath, string LocalRoot)
|
|
{
|
|
return LocalPath.Replace("\\", "/").Replace(LocalRoot, "../../..");
|
|
}
|
|
|
|
public override bool IsSupported { get { return true; } }
|
|
|
|
public override PakType RequiresPak(ProjectParams Params)
|
|
{
|
|
// if packaging is enabled, always create a pak, otherwise use the Params.Pak value
|
|
return Params.Package ? PakType.Always : PakType.DontCare;
|
|
}
|
|
public override bool SupportsMultiDeviceDeploy
|
|
{
|
|
get
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
public override bool RequiresPackageToDeploy
|
|
{
|
|
get { return true; }
|
|
}
|
|
*/
|
|
|
|
public override List<string> GetDebugFileExtensions()
|
|
{
|
|
return new List<string> { };
|
|
}
|
|
|
|
public override void StripSymbols(FileReference SourceFile, FileReference TargetFile)
|
|
{
|
|
AndroidExports.StripSymbols(SourceFile, TargetFile, Log.Logger);
|
|
}
|
|
}
|
|
|
|
public class AndroidPlatformMulti : AndroidPlatform
|
|
{
|
|
public override string GetCookPlatform(bool bDedicatedServer, bool bIsClientOnly)
|
|
{
|
|
return bIsClientOnly ? "Android_MultiClient" : "Android_Multi";
|
|
}
|
|
public override TargetPlatformDescriptor GetTargetPlatformDescriptor()
|
|
{
|
|
return new TargetPlatformDescriptor(TargetPlatformType, "Multi");
|
|
}
|
|
}
|
|
|
|
public class AndroidPlatformDXT : AndroidPlatform
|
|
{
|
|
public override string GetCookPlatform(bool bDedicatedServer, bool bIsClientOnly)
|
|
{
|
|
return bIsClientOnly ? "Android_DXTClient" : "Android_DXT";
|
|
}
|
|
|
|
public override TargetPlatformDescriptor GetTargetPlatformDescriptor()
|
|
{
|
|
return new TargetPlatformDescriptor(TargetPlatformType, "DXT");
|
|
}
|
|
}
|
|
public class AndroidPlatformETC2 : AndroidPlatform
|
|
{
|
|
public override string GetCookPlatform(bool bDedicatedServer, bool bIsClientOnly)
|
|
{
|
|
return bIsClientOnly ? "Android_ETC2Client" : "Android_ETC2";
|
|
}
|
|
public override TargetPlatformDescriptor GetTargetPlatformDescriptor()
|
|
{
|
|
return new TargetPlatformDescriptor(TargetPlatformType, "ETC2");
|
|
}
|
|
}
|
|
|
|
public class AndroidPlatformASTC : AndroidPlatform
|
|
{
|
|
public override string GetCookPlatform(bool bDedicatedServer, bool bIsClientOnly)
|
|
{
|
|
return bIsClientOnly ? "Android_ASTCClient" : "Android_ASTC";
|
|
}
|
|
public override TargetPlatformDescriptor GetTargetPlatformDescriptor()
|
|
{
|
|
return new TargetPlatformDescriptor(TargetPlatformType, "ASTC");
|
|
}
|
|
}
|
|
|
|
public class AndroidPlatformOpenXR : AndroidPlatform
|
|
{
|
|
public override string GetCookPlatform(bool bDedicatedServer, bool bIsClientOnly)
|
|
{
|
|
return bIsClientOnly ? "Android_OpenXRClient" : "Android_OpenXR";
|
|
}
|
|
public override TargetPlatformDescriptor GetTargetPlatformDescriptor()
|
|
{
|
|
return new TargetPlatformDescriptor(TargetPlatformType, "OpenXR");
|
|
}
|
|
}
|