2051 lines
67 KiB
C#
2051 lines
67 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using AutomationTool;
|
|
using UnrealBuildTool;
|
|
using System.Text.RegularExpressions;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using EpicGames.Core;
|
|
using System.Diagnostics;
|
|
using static AutomationTool.CommandUtils;
|
|
|
|
namespace Gauntlet
|
|
{
|
|
/// <summary>
|
|
/// Android implementation of a device that can run applications
|
|
/// </summary>
|
|
public class TargetDeviceAndroid : ITargetDevice
|
|
{
|
|
#region ITargetDevice
|
|
public string Name { get; protected set; }
|
|
|
|
public UnrealTargetPlatform? Platform => UnrealTargetPlatform.Android;
|
|
|
|
public CommandUtils.ERunOptions RunOptions { get; set; }
|
|
|
|
public bool IsConnected => IsAvailable;
|
|
|
|
public bool IsOn
|
|
{
|
|
get
|
|
{
|
|
string CommandLine = "shell dumpsys power";
|
|
IProcessResult OnAndUnlockedQuery = RunAdbDeviceCommand(CommandLine, bPauseErrorParsing: true);
|
|
return OnAndUnlockedQuery.Output.Contains("mHoldingDisplaySuspendBlocker=true")
|
|
&& OnAndUnlockedQuery.Output.Contains("mHoldingWakeLockSuspendBlocker=true");
|
|
}
|
|
}
|
|
|
|
public bool IsAvailable
|
|
{
|
|
get
|
|
{
|
|
// ensure our device is present in 'adb devices' output.
|
|
var AllDevices = GetAllConnectedDevices();
|
|
|
|
if (AllDevices.Keys.Contains(DeviceName) == false)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (AllDevices[DeviceName] != "device")
|
|
{
|
|
Log.Warning(KnownLogEvents.Gauntlet_DeviceEvent, "Device {Name} is '{State}'", DeviceName, AllDevices[DeviceName]);
|
|
return false;
|
|
}
|
|
|
|
// any device will do, but only one at a time.
|
|
return true;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Low-level device name
|
|
/// </summary>
|
|
public string DeviceName { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// Temp path we use to push/pull things from the device
|
|
/// </summary>
|
|
public string LocalCachePath { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// Root storage path of the device, e.g. /sdcard/
|
|
/// </summary>
|
|
public string StoragePath { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// External storage path on the device, e.g. /sdcard/UEGame/ etc..
|
|
/// </summary>
|
|
public string DeviceExternalStorageSavedPath { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// External files path on the device, this is the app's own publically accessible data directory e.g. /sdcard/Android/data/[com.package.name]/files/UnrealGame etc..
|
|
/// </summary>
|
|
public string DeviceExternalFilesSavedPath { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// Saved storage path, e.g. /sdcard/UEGame/Saved (bulk) or /sdcard/Android/data/[com.package.name]/files/UEGame/Saved (notbulk)
|
|
/// </summary>
|
|
public string DeviceArtifactPath { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// Path to the log file. (This takes public logs setting of the package in to account.)
|
|
/// </summary>
|
|
public string DeviceLogPath { get; protected set; }
|
|
|
|
[AutoParam((int)(60 * 15))]
|
|
public int MaxInstallTime { get; protected set; }
|
|
|
|
/// <summary>
|
|
/// Path to a command line if installed
|
|
/// </summary>
|
|
protected string CommandLineFilePath { get; set; }
|
|
|
|
protected bool IsExistingDevice = false;
|
|
|
|
protected Dictionary<EIntendedBaseCopyDirectory, string> LocalDirectoryMappings;
|
|
|
|
private string NoPlayProtectSetting = null;
|
|
|
|
public TargetDeviceAndroid(string InDeviceName = "", AndroidDeviceData DeviceData = null, string InCachePath = null)
|
|
{
|
|
AutoParam.ApplyParamsAndDefaults(this, Globals.Params.AllArguments);
|
|
|
|
AdbCredentialCache.AddInstance(DeviceData);
|
|
|
|
// If no device name or its 'default' then use the first default device
|
|
if (string.IsNullOrEmpty(InDeviceName) || InDeviceName.Equals("default", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var DefaultDevices = GetAllAvailableDevices();
|
|
|
|
if (DefaultDevices.Count() == 0)
|
|
{
|
|
if (GetAllConnectedDevices().Count > 0)
|
|
{
|
|
throw new AutomationException("No default device available. One or more devices are connected but unauthorized or offline. See 'adb devices'");
|
|
}
|
|
else
|
|
{
|
|
throw new AutomationException("No default device available. See 'adb devices'");
|
|
}
|
|
}
|
|
|
|
DeviceName = DefaultDevices.First();
|
|
}
|
|
else
|
|
{
|
|
DeviceName = InDeviceName;
|
|
}
|
|
|
|
if (Log.IsVerbose)
|
|
{
|
|
RunOptions = CommandUtils.ERunOptions.NoWaitForExit;
|
|
}
|
|
else
|
|
{
|
|
RunOptions = CommandUtils.ERunOptions.NoWaitForExit | CommandUtils.ERunOptions.NoLoggingOfRunCommand;
|
|
}
|
|
|
|
// if this is not a connected device then remove when done
|
|
var ConnectedDevices = GetAllConnectedDevices();
|
|
|
|
IsExistingDevice = ConnectedDevices.Keys.Contains(DeviceName);
|
|
if (!IsExistingDevice && !DeviceName.Contains(":"))
|
|
{
|
|
// adb uses port 5555 by default
|
|
DeviceName += ":5555";
|
|
IsExistingDevice = ConnectedDevices.Keys.Contains(DeviceName);
|
|
}
|
|
if (!IsExistingDevice)
|
|
{
|
|
lock (Globals.MainLock)
|
|
{
|
|
using (var PauseEC = new ScopedSuspendECErrorParsing())
|
|
{
|
|
IProcessResult AdbResult = RunAdbGlobalCommand(string.Format("connect {0}", DeviceName));
|
|
|
|
if (AdbResult.ExitCode != 0)
|
|
{
|
|
throw new DeviceException("adb failed to connect to {0}. {1}", DeviceName, AdbResult.Output);
|
|
}
|
|
}
|
|
|
|
Log.Info("Connecting to {0}", DeviceName);
|
|
|
|
// Need to sleep for adb service process to register, otherwise get an unauthorized (especially on parallel device use)
|
|
Thread.Sleep(5000);
|
|
}
|
|
|
|
ConnectedDevices = GetAllConnectedDevices();
|
|
|
|
// sanity check that it was now found
|
|
if (!ConnectedDevices.Keys.Contains(DeviceName))
|
|
{
|
|
throw new DeviceException("Failed to find new device {0} in connection list", DeviceName);
|
|
}
|
|
}
|
|
|
|
if (ConnectedDevices[DeviceName] != "device")
|
|
{
|
|
Dispose();
|
|
throw new DeviceException("Device {0} is '{1}'.", DeviceName, ConnectedDevices[DeviceName]);
|
|
}
|
|
|
|
// Get the external storage path from the device (once the device is validated as connected)
|
|
IProcessResult StorageQueryResult = RunAdbDeviceCommand(AndroidPlatform.GetStorageQueryCommand());
|
|
if (StorageQueryResult.ExitCode != 0)
|
|
{
|
|
throw new DeviceException("Failed to query external storage setup on {0}", DeviceName);
|
|
}
|
|
StoragePath = StorageQueryResult.Output.Trim();
|
|
|
|
LocalDirectoryMappings = new Dictionary<EIntendedBaseCopyDirectory, string>();
|
|
|
|
// for IP devices need to sanitize this
|
|
Name = DeviceName.Replace(":", "_");
|
|
|
|
// Temp path used when pulling artifact
|
|
LocalCachePath = InCachePath ?? Path.Combine(Globals.TempDir, "AndroidDevice_" + Name);
|
|
}
|
|
|
|
~TargetDeviceAndroid()
|
|
{
|
|
Dispose(false);
|
|
}
|
|
|
|
#region IDisposable Support
|
|
private bool DisposedValue = false; // To detect redundant calls
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (!DisposedValue)
|
|
{
|
|
try
|
|
{
|
|
if (!string.IsNullOrEmpty(NoPlayProtectSetting))
|
|
{
|
|
Log.Verbose("Restoring play protect for this session...");
|
|
RunAdbDeviceCommand($"shell settings put global package_verifier_user_consent {NoPlayProtectSetting}");
|
|
}
|
|
|
|
if (!IsExistingDevice)
|
|
{
|
|
// disconnect
|
|
RunAdbGlobalCommand(string.Format("disconnect {0}", DeviceName), true, false, true);
|
|
|
|
Log.Info("Disconnected {0}", DeviceName);
|
|
}
|
|
}
|
|
catch (Exception Ex)
|
|
{
|
|
Log.Warning(KnownLogEvents.Gauntlet_DeviceEvent, "TargetDeviceAndroid.Dispose() threw: {Exception}", Ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
DisposedValue = true;
|
|
AdbCredentialCache.RemoveInstance();
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// This code added to correctly implement the disposable pattern.
|
|
public void Dispose()
|
|
{
|
|
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
|
|
Dispose(true);
|
|
// TODO: uncomment the following line if the finalizer is overridden above.
|
|
// GC.SuppressFinalize(this);
|
|
}
|
|
|
|
public bool Disposed
|
|
{
|
|
get
|
|
{
|
|
return DisposedValue;
|
|
}
|
|
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ITargetDevice
|
|
public bool Connect()
|
|
{
|
|
AllowDeviceSleepState(true);
|
|
return true;
|
|
}
|
|
|
|
public bool Disconnect(bool bForce = false)
|
|
{
|
|
AllowDeviceSleepState(false);
|
|
return true;
|
|
}
|
|
|
|
public bool PowerOn()
|
|
{
|
|
Log.Verbose("{0}: Powering on", ToString());
|
|
string CommandLine = "shell \"input keyevent KEYCODE_WAKEUP && input keyevent KEYCODE_MENU\"";
|
|
IProcessResult PowerOnQuery = RunAdbDeviceCommand(CommandLine, bPauseErrorParsing: true);
|
|
return !PowerOnQuery.Output.Contains("error");
|
|
}
|
|
public bool PowerOff()
|
|
{
|
|
Log.Verbose("{0}: Powering off", ToString());
|
|
string CommandLine = "shell \"input keyevent KEYCODE_SLEEP\"";
|
|
IProcessResult PowerOffQuery = RunAdbDeviceCommand(CommandLine, bPauseErrorParsing: true);
|
|
return !PowerOffQuery.Output.Contains("error");
|
|
}
|
|
|
|
public bool Reboot()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public Dictionary<EIntendedBaseCopyDirectory, string> GetPlatformDirectoryMappings()
|
|
{
|
|
return LocalDirectoryMappings;
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
// TODO: device id
|
|
if (Name == DeviceName)
|
|
{
|
|
return Name;
|
|
}
|
|
return string.Format("{0} ({1})", Name, DeviceName);
|
|
}
|
|
|
|
public void FullClean()
|
|
{
|
|
string StorageLocation = RunAdbDeviceCommand(AndroidPlatform.GetStorageQueryCommand()).Output.Trim();
|
|
|
|
// Clean up any basic UE/System related directories
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}/UnrealGame/*", StorageLocation));
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}/Download/*", StorageLocation));
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}/gdeps/*", StorageLocation));
|
|
|
|
// Now find all third party packages/obbs and remove them
|
|
string PackageOutput = RunAdbDeviceCommandAndGetOutput("shell pm list packages -3");
|
|
|
|
// Ex. package:com.epicgames.lyra\r\n
|
|
Regex PackageRegex = new Regex("(package:)(.*)(\\r\\n)");
|
|
IEnumerable<string> Packages = PackageRegex.Matches(PackageOutput).Select(Match => Match.Groups[2].Value);
|
|
|
|
foreach (string Package in Packages)
|
|
{
|
|
Log.Info("Uninstalling {PackageName}...", Package);
|
|
RunAdbDeviceCommand(string.Format("uninstall {0}", Package));
|
|
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}/Android/data/{1}", StorageLocation, Package));
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}/Android/obb/{1}", StorageLocation, Package));
|
|
}
|
|
}
|
|
|
|
public void CleanArtifacts(UnrealAppConfig AppConfig = null)
|
|
{
|
|
if (AppConfig == null)
|
|
{
|
|
Log.Warning("{Platform} expects an non-null AppConfig value to reliably determine which artifact path to clear! Skipping clean", Platform);
|
|
return;
|
|
}
|
|
|
|
if (AppConfig.Build is not AndroidBuild Build)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string ExternalStoragePath = StoragePath + "/UnrealGame/" + AppConfig.ProjectName;
|
|
string ExternalFilesPath = StoragePath + "/Android/data/" + Build.AndroidPackageName + "/files/UnrealGame/" + AppConfig.ProjectName;
|
|
|
|
string ExternalAppStorage = string.Format("{0}/{1}/Saved", ExternalStoragePath, AppConfig.ProjectName);
|
|
string ExternalAppFiles = string.Format("{0}/{1}/Saved", ExternalFilesPath, AppConfig.ProjectName);
|
|
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}/*", ExternalAppStorage));
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}/*", ExternalAppFiles));
|
|
}
|
|
|
|
public void InstallBuild(UnrealAppConfig AppConfig)
|
|
{
|
|
AndroidBuild Build = AppConfig.Build as AndroidBuild;
|
|
if (Build == null)
|
|
{
|
|
throw new AutomationException("Unsupported build type {0} for Android!", AppConfig.Build.GetType());
|
|
}
|
|
|
|
string Package = Build.AndroidPackageName;
|
|
KillRunningProcess(Package);
|
|
|
|
// Install apk
|
|
string APK = Globals.IsRunningDev && AppConfig.OverlayExecutable.GetOverlay(Build.SourceApkPath, out string OverlayAPK)
|
|
? OverlayAPK
|
|
: Build.SourceApkPath;
|
|
CopyFileToDevice(Package, APK, string.Empty);
|
|
|
|
EnablePermissions(Package);
|
|
|
|
// Copy obbs from bulk builds
|
|
bool bSkipOBBInstall = Globals.Params.ParseParam("SkipOBBCopy"); // useful when iterating on dev executables
|
|
if(Build.FilesToInstall != null && Build.FilesToInstall.Any() && !bSkipOBBInstall)
|
|
{
|
|
CopyOBBFiles(AppConfig, Build.FilesToInstall, Package);
|
|
}
|
|
}
|
|
|
|
public IAppInstall CreateAppInstall(UnrealAppConfig AppConfig)
|
|
{
|
|
if(AppConfig.Build is not AndroidBuild Build)
|
|
{
|
|
throw new AutomationException("Unsupported build type {0} for Android!", AppConfig.Build.GetType());
|
|
}
|
|
|
|
string ExternalStoragePath = StoragePath + "/UnrealGame/" + AppConfig.ProjectName;
|
|
string ExternalFilesPath = StoragePath + "/Android/data/" + Build.AndroidPackageName + "/files/UnrealGame/" + AppConfig.ProjectName;
|
|
|
|
DeviceExternalStorageSavedPath = string.Format("{0}/{1}/Saved", ExternalStoragePath, AppConfig.ProjectName);
|
|
DeviceExternalFilesSavedPath = string.Format("{0}/{1}/Saved", ExternalFilesPath, AppConfig.ProjectName);
|
|
DeviceLogPath = string.Format("{0}/Logs/{1}.log", Build.UsesPublicLogs ? DeviceExternalFilesSavedPath : DeviceExternalStorageSavedPath, AppConfig.ProjectName);
|
|
DeviceArtifactPath = Build.UsesExternalFilesDir ? DeviceExternalFilesSavedPath : DeviceExternalStorageSavedPath;
|
|
|
|
PopulateDirectoryMappings(Path.GetDirectoryName(DeviceArtifactPath));
|
|
|
|
return new AndroidAppInstall(AppConfig, this, AppConfig.ProjectName, Build.AndroidPackageName, AppConfig.CommandLine, AppConfig.ProjectFile, AppConfig.Build.Configuration);
|
|
}
|
|
|
|
public void CopyAdditionalFiles(IEnumerable<UnrealFileToCopy> FilesToCopy)
|
|
{
|
|
foreach (UnrealFileToCopy FileToCopy in FilesToCopy)
|
|
{
|
|
string DestinationFile = string.Format("{0}/{1}", LocalDirectoryMappings[FileToCopy.TargetBaseDirectory], FileToCopy.TargetRelativeLocation);
|
|
if (File.Exists(FileToCopy.SourceFileLocation))
|
|
{
|
|
FileInfo SourceFile = new FileInfo(FileToCopy.SourceFileLocation);
|
|
SourceFile.IsReadOnly = false;
|
|
CopyFileToDevice(null, SourceFile.FullName, DestinationFile); // todo: add AFS support
|
|
}
|
|
else
|
|
{
|
|
Log.Warning(KnownLogEvents.Gauntlet_DeviceEvent, "File to copy {File} not found", FileToCopy);
|
|
}
|
|
}
|
|
}
|
|
|
|
public IAppInstance Run(IAppInstall App)
|
|
{
|
|
if (App is not AndroidAppInstall Install)
|
|
{
|
|
throw new AutomationException("AppInstall is of incorrect type {0}! Must be of type AndroidAppInstall", App.GetType().Name);
|
|
}
|
|
|
|
if(!IsOn)
|
|
{
|
|
PowerOn();
|
|
}
|
|
|
|
// Now, create and push the UECommandline.txt file
|
|
bool bUseExternalFileDirectory = (Install.AppConfig.Build as AndroidBuild)?.UsesExternalFilesDir ?? false;
|
|
CopyCommandlineFile(Install.AppConfig, Install.AppConfig.CommandLine, Install.AndroidPackageName, Install.AppConfig.ProjectName, bUseExternalFileDirectory);
|
|
|
|
// kill any currently running instance:
|
|
KillRunningProcess(Install.AndroidPackageName);
|
|
|
|
string LaunchActivity = AndroidPlatform.GetLaunchableActivityName(Install.ApkPath);
|
|
|
|
Log.Info("Launching {0} on '{1}' ", Install.AndroidPackageName + "/" + LaunchActivity, ToString());
|
|
Log.Verbose("\t{0}", Install.CommandLine);
|
|
|
|
// Clear the device's logcat in preparation for the test..
|
|
RunAdbDeviceCommand("logcat --clear");
|
|
|
|
// Ensure artifact directories exist
|
|
if (UsingAndroidFileServer(Install.ProjectFile, Install.Configuration, out _, out string AFSToken, out _, out _, out _))
|
|
{
|
|
RunAFSDeviceCommand(string.Format("-p \"{0}\" -k \"{1}\" mkdir \"^saved\"", Install.AndroidPackageName, AFSToken));
|
|
}
|
|
else
|
|
{
|
|
RunAdbDeviceCommand(string.Format("shell mkdir -p {0}/", DeviceExternalStorageSavedPath));
|
|
RunAdbDeviceCommand(string.Format("shell mkdir -p {0}/", DeviceExternalFilesSavedPath));
|
|
}
|
|
|
|
// start the app on device!
|
|
string CommandLine = "shell am start -W -S -n " + Install.AndroidPackageName + "/" + LaunchActivity;
|
|
IProcessResult Process = RunAdbDeviceCommand(CommandLine, false, true);
|
|
|
|
return new AndroidAppInstance(this, Install, Process);
|
|
}
|
|
#endregion
|
|
|
|
public void PopulateDirectoryMappings(string ProjectDir)
|
|
{
|
|
LocalDirectoryMappings.Clear();
|
|
|
|
ProjectDir = ProjectDir.Replace('\\', '/');
|
|
if (!ProjectDir.EndsWith('/'))
|
|
{
|
|
ProjectDir += '/';
|
|
}
|
|
|
|
LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Build, ProjectDir + "Build");
|
|
LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Binaries, ProjectDir + "Binaries");
|
|
LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Config, ProjectDir + "Saved/Config");
|
|
LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Content, ProjectDir + "Content");
|
|
LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Demos, ProjectDir + "Saved/Demos");
|
|
LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.PersistentDownloadDir, ProjectDir + "Saved/PersistentDownloadDir");
|
|
LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Profiling, ProjectDir + "Saved/Profiling");
|
|
LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Saved, ProjectDir + "Saved");
|
|
}
|
|
|
|
public static bool UsingAndroidFileServer(FileReference RawProjectPath, UnrealTargetConfiguration TargetConfiguration, out bool bEnablePlugin, out string AFSToken, out bool bIsShipping, out bool bIncludeInShipping, out bool bAllowExternalStartInShipping)
|
|
{
|
|
if(RawProjectPath == null || !FileReference.Exists(RawProjectPath) || TargetConfiguration == UnrealTargetConfiguration.Unknown)
|
|
{
|
|
bEnablePlugin = false;
|
|
AFSToken = string.Empty;
|
|
bIsShipping = false;
|
|
bIncludeInShipping = false;
|
|
bAllowExternalStartInShipping = false;
|
|
return false;
|
|
}
|
|
|
|
UnrealTargetPlatform TargetPlatform = UnrealTargetPlatform.Android;
|
|
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;
|
|
}
|
|
|
|
public bool UseAFS(UnrealAppConfig AppConfig)
|
|
{
|
|
AndroidBuild Build = AppConfig.Build as AndroidBuild;
|
|
if (Build == null)
|
|
{
|
|
throw new AutomationException("Unsupported build type {0} for Android!", AppConfig.Build.GetType());
|
|
}
|
|
|
|
return UsingAndroidFileServer(AppConfig.ProjectFile, Build.Configuration, out _, out _, out _, out _, out _);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs an ADB command, automatically adding the name of the current device to
|
|
/// the arguments sent to adb
|
|
/// </summary>
|
|
/// <param name="Args"></param>
|
|
/// <param name="Wait"></param>
|
|
/// <param name="bShouldLogCommand"></param>
|
|
/// <param name="bPauseErrorParsing"></param>
|
|
/// <returns></returns>
|
|
public IProcessResult RunAdbDeviceCommand(string Args, bool Wait = true, bool bShouldLogCommand = false, bool bPauseErrorParsing = false, int TimeoutSec = 60 * 15)
|
|
{
|
|
if (string.IsNullOrEmpty(DeviceName) == false)
|
|
{
|
|
Args = string.Format("-s {0} {1}", DeviceName, Args);
|
|
}
|
|
|
|
return RunAdbGlobalCommand(Args, Wait, bShouldLogCommand, bPauseErrorParsing, TimeoutSec);
|
|
}
|
|
|
|
public IProcessResult RunAFSDeviceCommand(string Args, bool Wait = true, bool bShouldLogCommand = false, bool bPauseErrorParsing = false, int TimeoutSec = 60 * 15)
|
|
{
|
|
if (string.IsNullOrEmpty(DeviceName) == false)
|
|
{
|
|
Args = string.Format("-s {0} {1}", DeviceName, Args);
|
|
}
|
|
|
|
return RunAFSGlobalCommand(Args, Wait, bShouldLogCommand, bPauseErrorParsing, TimeoutSec);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs an ADB command, automatically adding the name of the current device to
|
|
/// the arguments sent to adb
|
|
/// </summary>
|
|
/// <param name="Args"></param>
|
|
/// <returns></returns>
|
|
public string RunAdbDeviceCommandAndGetOutput(string Args)
|
|
{
|
|
if (string.IsNullOrEmpty(DeviceName) == false)
|
|
{
|
|
Args = string.Format("-s {0} {1}", DeviceName, Args);
|
|
}
|
|
|
|
IProcessResult Result = RunAdbGlobalCommand(Args);
|
|
|
|
if (Result.ExitCode != 0)
|
|
{
|
|
throw new DeviceException("adb command {0} failed. {1}", Args, Result.Output);
|
|
}
|
|
|
|
return Result.Output;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs an ADB command at the global scope
|
|
/// </summary>
|
|
/// <param name="Args"></param>
|
|
/// <param name="Wait"></param>
|
|
/// <param name="bShouldLogCommand"></param>
|
|
/// <param name="bPauseErrorParsing"></param>
|
|
/// <returns></returns>
|
|
public static IProcessResult RunAdbGlobalCommand(string Args, bool Wait = true, bool bShouldLogCommand = false, bool bPauseErrorParsing = false, int TimeoutSec = 60 * 15)
|
|
{
|
|
CommandUtils.ERunOptions RunOptions = CommandUtils.ERunOptions.AppMustExist | CommandUtils.ERunOptions.NoWaitForExit | CommandUtils.ERunOptions.SpewIsVerbose;
|
|
RunOptions |= Log.IsVeryVerbose? ERunOptions.AllowSpew : ERunOptions.NoLoggingOfRunCommand;
|
|
|
|
if (bShouldLogCommand)
|
|
{
|
|
Log.Verbose("Running ADB Command: adb {0}", Args);
|
|
}
|
|
|
|
IProcessResult Process;
|
|
|
|
using (bPauseErrorParsing ? new ScopedSuspendECErrorParsing() : null)
|
|
{
|
|
Process = AndroidPlatform.RunAdbCommand(null, null, Args, null, RunOptions);
|
|
|
|
if (Wait)
|
|
{
|
|
Process.ProcessObject.WaitForExit(TimeoutSec * 1000);
|
|
|
|
if (!Process.HasExited)
|
|
{
|
|
Log.Info("ADB Command 'adb {Args}' timed out after {Timeout} sec.", Args, TimeoutSec);
|
|
|
|
Process.ProcessObject?.Kill();
|
|
Process.ProcessObject?.WaitForExit(15000);
|
|
}
|
|
}
|
|
}
|
|
|
|
return Process;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Run Adb command without waiting for exit
|
|
/// </summary>
|
|
/// <param name="Args"></param>
|
|
/// <returns></returns>
|
|
public ILongProcessResult RunAdbCommandNoWait(string Args, string LocalCache)
|
|
{
|
|
if (string.IsNullOrEmpty(DeviceName) == false)
|
|
{
|
|
Args = string.Format("-s {0} {1}", DeviceName, Args);
|
|
}
|
|
CommandUtils.ERunOptions RunOptions = CommandUtils.ERunOptions.AppMustExist | CommandUtils.ERunOptions.NoWaitForExit | CommandUtils.ERunOptions.SpewIsVerbose;
|
|
RunOptions |= Log.IsVeryVerbose ? ERunOptions.AllowSpew : ERunOptions.NoLoggingOfRunCommand;
|
|
|
|
string AdbCommand = Environment.ExpandEnvironmentVariables("%ANDROID_HOME%/platform-tools/adb" + (OperatingSystem.IsWindows() ? ".exe" : ""));
|
|
return new LongProcessResult(AdbCommand, Args, RunOptions, LocalCache: LocalCache);
|
|
}
|
|
|
|
public static IProcessResult RunAFSGlobalCommand(string Args, bool Wait = true, bool bShouldLogCommand = false, bool bPauseErrorParsing = false, int TimeoutSec = 60 * 15 )
|
|
{
|
|
CommandUtils.ERunOptions RunOptions = CommandUtils.ERunOptions.AppMustExist | CommandUtils.ERunOptions.NoWaitForExit | CommandUtils.ERunOptions.SpewIsVerbose;
|
|
|
|
if (Log.IsVeryVerbose)
|
|
{
|
|
RunOptions |= CommandUtils.ERunOptions.AllowSpew;
|
|
}
|
|
else
|
|
{
|
|
RunOptions |= CommandUtils.ERunOptions.NoLoggingOfRunCommand;
|
|
}
|
|
|
|
if (bShouldLogCommand)
|
|
{
|
|
Log.Verbose("Running AFS Command: UnrealAndroidFileTool {0}", Args);
|
|
}
|
|
|
|
IProcessResult Process;
|
|
|
|
using (bPauseErrorParsing ? new ScopedSuspendECErrorParsing() : null)
|
|
{
|
|
Process = AndroidPlatform.RunAFSCommand(null, null, Args, null, RunOptions);
|
|
|
|
if (Wait)
|
|
{
|
|
Process.ProcessObject.WaitForExit(TimeoutSec * 1000);
|
|
|
|
if (!Process.HasExited)
|
|
{
|
|
Log.Info("AFS Command 'UnrealAndroidFileTool {Args}' timed out after {Timeout} sec.", Args, TimeoutSec);
|
|
|
|
Process.ProcessObject?.Kill();
|
|
Process.ProcessObject?.WaitForExit(15000);
|
|
}
|
|
}
|
|
}
|
|
|
|
return Process;
|
|
}
|
|
|
|
|
|
public static ITargetDevice[] GetDefaultDevices()
|
|
{
|
|
return GetAllAvailableDevices().Select(Device => new TargetDeviceAndroid(Device)).ToArray();
|
|
}
|
|
|
|
public void PostRunCleanup(UnrealAppConfig AppConfig, string Package)
|
|
{
|
|
// Delete the commandline file, if someone installs an APK on top of ours
|
|
// they will get very confusing behavior...
|
|
if (string.IsNullOrEmpty(CommandLineFilePath) == false)
|
|
{
|
|
Log.Verbose("Removing {0}", CommandLineFilePath);
|
|
if (UseAFS(AppConfig))
|
|
{
|
|
DeleteFileFromDevice(Package, "^commandfile", AppConfig.ProjectFile, AppConfig.Configuration);
|
|
}
|
|
else
|
|
{
|
|
DeleteFileFromDevice(Package, CommandLineFilePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
public string AFSGetFileInfo(string PackageName, string AFSToken, string FileName)
|
|
{
|
|
IProcessResult result;
|
|
if (FileName.StartsWith("^"))
|
|
{
|
|
// Query to get which file this actually is.
|
|
string ActualFileName = "";
|
|
result = RunAFSDeviceCommand(string.Format("-p \"{0}\" -k \"{1}\" query", PackageName, AFSToken));
|
|
string[] mappings = result.Output.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).Select(S => S.Trim()).ToArray();
|
|
foreach (string mapping in mappings)
|
|
{
|
|
string[] FromTo = mapping.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries).Select(S => S.Trim()).ToArray();
|
|
if (FromTo.Length != 2)
|
|
{
|
|
continue;
|
|
}
|
|
if (FromTo[0] == FileName)
|
|
{
|
|
ActualFileName = Path.GetFileName(FromTo[1]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(ActualFileName))
|
|
{
|
|
return "";
|
|
//throw new AutomationException("Failed to find file {0} in AFS mappings", FileName);
|
|
}
|
|
|
|
// Now List all the files in ^obb and find our file
|
|
result = RunAFSDeviceCommand(string.Format("-p \"{0}\" -k \"{1}\" ls -ls \"^obb\"", PackageName, AFSToken));
|
|
string[] ObbFiles = result.Output.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).Select(S => S.Trim()).ToArray();
|
|
foreach (string file in ObbFiles)
|
|
{
|
|
if (file.Contains(ActualFileName))
|
|
{
|
|
return file;
|
|
}
|
|
}
|
|
|
|
// Now List all the files in ^project and find our file
|
|
result = RunAFSDeviceCommand(string.Format("-p \"{0}\" -k \"{1}\" ls -ls \"^project\"", PackageName, AFSToken));
|
|
string[] ProjectFiles = result.Output.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).Select(S => S.Trim()).ToArray();
|
|
foreach (string file in ProjectFiles)
|
|
{
|
|
if (file.Contains(ActualFileName))
|
|
{
|
|
return file;
|
|
}
|
|
}
|
|
|
|
//throw new AutomationException("Failed to find file {0} in AFS ^obb", FileName);
|
|
return "";
|
|
}
|
|
else
|
|
{
|
|
//throw new AutomationException("Unsupported file {0} for AFS", FileName);
|
|
return "";
|
|
}
|
|
}
|
|
|
|
public bool CopyFileToDevice(string PackageName, string SourcePath, string DestPath,
|
|
FileReference ProjectFile = null, UnrealTargetConfiguration Configuration = UnrealTargetConfiguration.Unknown, bool IgnoreDependencies = false)
|
|
{
|
|
string AFSToken;
|
|
bool UseAFS = UsingAndroidFileServer(ProjectFile, Configuration, out _, out AFSToken, out _, out _, out _);
|
|
bool IsAPK = string.Equals(Path.GetExtension(SourcePath), ".apk", StringComparison.OrdinalIgnoreCase);
|
|
|
|
// for the APK there's no easy/reliable way to get the date of the version installed, so
|
|
// we write this out to a dependency file in the demote dir and check it each time.
|
|
// current file time
|
|
DateTime LocalModifiedTime = File.GetLastWriteTime(SourcePath);
|
|
|
|
string QuotedSourcePath = SourcePath;
|
|
if (SourcePath.Contains(" "))
|
|
{
|
|
QuotedSourcePath = '"' + SourcePath + '"';
|
|
}
|
|
|
|
// dependency info is a hash of the destination name, saved under a folder on /sdcard
|
|
string DestHash = ContentHash.MD5(DestPath).ToString();
|
|
string DependencyCacheDir = "/sdcard/gdeps";
|
|
string DepFile = string.Format("{0}/{1}", DependencyCacheDir, DestHash);
|
|
|
|
IProcessResult AdbResult = null;
|
|
|
|
|
|
// get info from the device about this file
|
|
string CurrentFileInfo = null;
|
|
|
|
if (IsAPK)
|
|
{
|
|
// for APK query the package info and get the update time
|
|
AdbResult = RunAdbDeviceCommand(string.Format("shell dumpsys package {0} | grep lastUpdateTime", PackageName));
|
|
|
|
if (AdbResult.ExitCode == 0)
|
|
{
|
|
CurrentFileInfo = AdbResult.Output.ToString().Trim();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// for other files get the file info
|
|
if (UseAFS)
|
|
{
|
|
CurrentFileInfo = AFSGetFileInfo(PackageName, AFSToken, DestPath);
|
|
}
|
|
else
|
|
{
|
|
AdbResult = RunAdbDeviceCommand(string.Format("shell ls -l {0}", DestPath));
|
|
if (AdbResult.ExitCode == 0)
|
|
{
|
|
CurrentFileInfo = AdbResult.Output.ToString().Trim();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
bool SkipInstall = false;
|
|
|
|
// If this is valid then there is some form of that file on the device, now figure out if it matches the
|
|
if (string.IsNullOrEmpty(CurrentFileInfo) == false)
|
|
{
|
|
// read the dep file
|
|
AdbResult = RunAdbDeviceCommand(string.Format("shell cat {0}", DepFile));
|
|
|
|
if (AdbResult.ExitCode == 0)
|
|
{
|
|
// Dependency info is the modified time of the source, and the post-copy file stats of the installed file, separated by ###
|
|
string[] DepLines = AdbResult.Output.ToString().Split(new[] { "###" }, StringSplitOptions.RemoveEmptyEntries).Select(S => S.Trim()).ToArray();
|
|
|
|
if (DepLines.Length >= 2)
|
|
{
|
|
string InstalledSourceModifiedTime = DepLines[0];
|
|
string InstalledFileInfo = DepLines[1];
|
|
|
|
if (InstalledSourceModifiedTime == LocalModifiedTime.ToString()
|
|
&& CurrentFileInfo == InstalledFileInfo)
|
|
{
|
|
SkipInstall = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (SkipInstall && IgnoreDependencies == false)
|
|
{
|
|
Log.Info("Skipping install of {0} - remote file up to date", Path.GetFileName(SourcePath));
|
|
}
|
|
else
|
|
{
|
|
if (IsAPK)
|
|
{
|
|
// we need to ununstall then install the apk - don't care if it fails, may have been deleted
|
|
string AdbCommand = string.Format("uninstall {0}", PackageName);
|
|
AdbResult = RunAdbDeviceCommand(AdbCommand);
|
|
|
|
Log.Info("Installing {0} to {1}", SourcePath, Name);
|
|
|
|
AdbCommand = string.Format("install {0}", QuotedSourcePath);
|
|
AdbResult = RunAdbDeviceCommand(AdbCommand);
|
|
|
|
if (AdbResult.ExitCode != 0)
|
|
{
|
|
throw new DeviceException("Failed to install {0}. Error {1}", SourcePath, AdbResult.Output);
|
|
}
|
|
|
|
// for APK query the package info and get the update time
|
|
AdbResult = RunAdbDeviceCommand(string.Format("shell dumpsys package {0} | grep lastUpdateTime", PackageName));
|
|
CurrentFileInfo = AdbResult.Output.ToString().Trim();
|
|
}
|
|
else
|
|
{
|
|
string FileDirectory = Path.GetDirectoryName(DestPath).Replace('\\', '/');
|
|
|
|
if (UseAFS)
|
|
{
|
|
Log.Info("Copying {0} to {1} via AFS", QuotedSourcePath, DestPath);
|
|
AdbResult = RunAFSDeviceCommand(string.Format("-p \"{0}\" -k \"{1}\" push \"{2}\" \"{3}\"", PackageName, AFSToken, SourcePath, DestPath), TimeoutSec: MaxInstallTime);
|
|
}
|
|
else
|
|
{
|
|
RunAdbDeviceCommand(string.Format("shell mkdir -p {0}/", FileDirectory));
|
|
|
|
Log.Info("Copying {0} to {1} via adb push", QuotedSourcePath, DestPath);
|
|
AdbResult = RunAdbDeviceCommand(string.Format("push {0} {1}", QuotedSourcePath, DestPath), TimeoutSec: MaxInstallTime);
|
|
}
|
|
|
|
// Note: Presently, AdbResult NEVER reports failures to push when using AFS. Should be fixed at some point.
|
|
|
|
if (AdbResult.ExitCode != 0)
|
|
{
|
|
if ((AdbResult.Output.Contains("couldn't read from device") || AdbResult.Output.Contains("offline")) && !IsConnected)
|
|
{
|
|
Log.Info("Lost connection with device '{Name}'.", Name);
|
|
// Disconnection occurred. Let's retry a second time before given up
|
|
// Try to reconnect
|
|
if (Connect())
|
|
{
|
|
if (UseAFS)
|
|
{
|
|
Log.Info("Retrying to copy via AFS...");
|
|
AdbResult = RunAFSDeviceCommand(string.Format("-p {0} -k {1} push \"{2}\" \"{3}\"", PackageName, AFSToken, SourcePath, DestPath), TimeoutSec: MaxInstallTime);
|
|
}
|
|
else
|
|
{
|
|
Log.Info("Retrying to copy via adb push...");
|
|
AdbResult = RunAdbDeviceCommand(string.Format("push {0} {1}", QuotedSourcePath, DestPath), TimeoutSec: MaxInstallTime);
|
|
}
|
|
if (AdbResult.ExitCode != 0)
|
|
{
|
|
throw new DeviceException("Failed to push {0} to device. Error {1}", SourcePath, AdbResult.Output);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new DeviceException("Failed to reconnect {0}", Name);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new DeviceException("Failed to push {0} to device. Error {1}", SourcePath, AdbResult.Output);
|
|
}
|
|
}
|
|
|
|
// Now pull info about the file which we'll write as a dep
|
|
if (UseAFS)
|
|
{
|
|
CurrentFileInfo = AFSGetFileInfo(PackageName, AFSToken, DestPath);
|
|
}
|
|
else
|
|
{
|
|
AdbResult = RunAdbDeviceCommand(string.Format("shell ls -l {0}", DestPath));
|
|
CurrentFileInfo = AdbResult.Output.ToString().Trim();
|
|
}
|
|
}
|
|
|
|
// write the actual dependency info
|
|
string DepContents = LocalModifiedTime + "###" + CurrentFileInfo;
|
|
|
|
// save last modified time to remote deps after success
|
|
RunAdbDeviceCommand(string.Format("shell mkdir -p {0}", DependencyCacheDir));
|
|
AdbResult = RunAdbDeviceCommand(string.Format("shell echo \"{0}\" > {1}", DepContents, DepFile));
|
|
|
|
if (AdbResult.ExitCode != 0)
|
|
{
|
|
Log.Warning(KnownLogEvents.Gauntlet_DeviceEvent, "Failed to write dependency file {File}", DepFile);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void AllowDeviceSleepState(bool bAllowSleep)
|
|
{
|
|
string CommandLine = "shell svc power stayon " + (bAllowSleep ? "false" : "usb");
|
|
RunAdbDeviceCommand(CommandLine, true, false, true);
|
|
}
|
|
/// <summary>
|
|
/// Enable Android permissions which would otherwise block automation with permission requests
|
|
/// </summary>
|
|
public void EnablePermissions(string AndroidPackageName)
|
|
{
|
|
List<string> Permissions = new List<string> { "POST_NOTIFICATIONS", "WRITE_EXTERNAL_STORAGE", "READ_EXTERNAL_STORAGE", "GET_ACCOUNTS", "RECORD_AUDIO" };
|
|
Permissions.ForEach(Permission =>
|
|
{
|
|
string CommandLine = string.Format("shell pm grant {0} android.permission.{1}", AndroidPackageName, Permission);
|
|
Log.Verbose(string.Format("Enabling permission: {0} {1}", AndroidPackageName, Permission));
|
|
RunAdbDeviceCommand(CommandLine, true, false, true);
|
|
});
|
|
Log.Verbose($"Enabling permission: {AndroidPackageName} MANAGE_EXTERNAL_STORAGE");
|
|
RunAdbDeviceCommand($"shell appops set {AndroidPackageName} MANAGE_EXTERNAL_STORAGE allow");
|
|
}
|
|
|
|
public void KillRunningProcess(string AndroidPackageName)
|
|
{
|
|
Log.Verbose("{0}: Killing process '{1}' ", ToString(), AndroidPackageName);
|
|
string KillProcessCommand = string.Format("shell am force-stop {0}", AndroidPackageName);
|
|
RunAdbDeviceCommand(KillProcessCommand);
|
|
}
|
|
|
|
protected void CopyOBBFiles(UnrealAppConfig AppConfig, Dictionary<string, string> OBBFiles, string Package)
|
|
{
|
|
bool bAFSEnablePlugin;
|
|
string AFSToken = "";
|
|
bool bIsShipping;
|
|
bool bAFSIncludeInShipping;
|
|
bool bAFSAllowExternalStartInShipping;
|
|
bool useADB = true;
|
|
if (AppConfig != null)
|
|
{
|
|
AndroidBuild Build = AppConfig.Build as AndroidBuild;
|
|
if (Build == null)
|
|
{
|
|
throw new AutomationException("Unsupported build type {0} for Android!", AppConfig.Build.GetType());
|
|
}
|
|
|
|
if (UsingAndroidFileServer(AppConfig.ProjectFile, Build.Configuration, out bAFSEnablePlugin, out AFSToken, out bIsShipping, out bAFSIncludeInShipping, out bAFSAllowExternalStartInShipping))
|
|
{
|
|
useADB = false;
|
|
}
|
|
}
|
|
|
|
if (useADB)
|
|
{
|
|
string OBBRemoteDestination = string.Format("{0}/Android/obb/{1}", StoragePath, Package);
|
|
|
|
// Remove all existing obbs
|
|
RunAdbDeviceCommand(string.Format("shell rm {0}", OBBRemoteDestination));
|
|
RunAdbDeviceCommand(string.Format("shell mkdir -p {0}", OBBRemoteDestination));
|
|
}
|
|
else
|
|
{
|
|
RunAFSDeviceCommand(string.Format("-p {0} -k {1} rm {2}", Package, AFSToken, "^mainobb"));
|
|
RunAFSDeviceCommand(string.Format("-p {0} -k {1} rm {2}", Package, AFSToken, "^patchobb"));
|
|
for(int i = 1; i <= 32; i++)
|
|
{
|
|
string overflowobb = string.Format("^overflow{0}obb", i);
|
|
RunAFSDeviceCommand(string.Format("-p {0} -k {1} rm {2}", Package, AFSToken, overflowobb));
|
|
}
|
|
}
|
|
|
|
// obb files need to be named based on APK version (grrr), so find that out. This should return something like
|
|
// versionCode=2 minSdk=21 targetSdk=21
|
|
string PackageInfo = RunAdbDeviceCommand(string.Format("shell dumpsys package {0} | grep versionCode", Package)).Output;
|
|
var Match = Regex.Match(PackageInfo, @"versionCode=([\d\.]+)\s");
|
|
if (Match.Success == false)
|
|
{
|
|
throw new AutomationException("Failed to find version info for APK!");
|
|
}
|
|
string PackageVersion = Match.Groups[1].ToString();
|
|
|
|
foreach (KeyValuePair<string, string> Pair in OBBFiles)
|
|
{
|
|
string SourceFile = Pair.Key;
|
|
string DestinationFile = Pair.Value;
|
|
|
|
if (useADB)
|
|
{
|
|
// If we installed a new APK we need to change the package version
|
|
Match OBBMatch = Regex.Match(SourceFile, @"\.(\d+)\.com.*\.obb");
|
|
if (OBBMatch.Success)
|
|
{
|
|
string StrippedObb = Path.GetFileName(SourceFile.Replace(".Client.obb", ".obb").Replace(OBBMatch.Groups[1].ToString(), PackageVersion));
|
|
DestinationFile = StoragePath + "/Android/obb/" + Package + "/" + StrippedObb;
|
|
}
|
|
|
|
DestinationFile = Regex.Replace(DestinationFile, "%STORAGE%", StoragePath, RegexOptions.IgnoreCase);
|
|
CopyFileToDevice(null, SourceFile, DestinationFile, AppConfig.ProjectFile, AppConfig.Configuration);
|
|
}
|
|
else
|
|
{
|
|
string Filename = Path.GetFileName(SourceFile);
|
|
string[] parts = Filename.Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries).Select(S => S.Trim()).ToArray();
|
|
// Make sure last part is obb
|
|
if (parts[parts.Length - 1] == "obb")
|
|
{
|
|
string obbname = string.Format("^{0}obb", parts[0]);
|
|
CopyFileToDevice(Package, SourceFile, obbname, AppConfig.ProjectFile, AppConfig.Configuration);
|
|
}
|
|
else
|
|
{
|
|
DestinationFile = Regex.Replace(DestinationFile, "%STORAGE%", StoragePath, RegexOptions.IgnoreCase);
|
|
CopyFileToDevice(Package, SourceFile, DestinationFile, AppConfig.ProjectFile, AppConfig.Configuration);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void CopyCommandlineFile(UnrealAppConfig AppConfig, string Commandline, string Package, string Project, bool bUseExternalFileDirectory)
|
|
{
|
|
string ExternalStoragePath = StoragePath + "/UnrealGame/" + Project;
|
|
string ExternalFilesPath = StoragePath + "/Android/data/" + Package + "/files/UnrealGame/" + Project;
|
|
string DeviceBaseDir = bUseExternalFileDirectory ? ExternalFilesPath : ExternalStoragePath;
|
|
CommandLineFilePath = string.Format("{0}/UECommandLine.txt", DeviceBaseDir);
|
|
|
|
PostRunCleanup(AppConfig, Package); // function needs a rename in this context, just delete any old UECommandlines
|
|
|
|
// Create a tempfile, insert the command line, and push it over
|
|
string CommandLineTmpFile = Path.GetTempFileName();
|
|
|
|
// I've seen a weird thing where adb push truncates by a byte, so add some padding...
|
|
File.WriteAllText(CommandLineTmpFile, Commandline + " ");
|
|
|
|
if (UseAFS(AppConfig))
|
|
{
|
|
CopyFileToDevice(Package, CommandLineTmpFile, "^commandfile", AppConfig.ProjectFile, AppConfig.Configuration);
|
|
}
|
|
else
|
|
{
|
|
CopyFileToDevice(Package, CommandLineTmpFile, CommandLineFilePath);
|
|
}
|
|
|
|
File.Delete(CommandLineTmpFile);
|
|
}
|
|
|
|
protected bool DeleteFileFromDevice(string Package, string DestPath,
|
|
FileReference ProjectFile = null, UnrealTargetConfiguration Configuration = UnrealTargetConfiguration.Unknown)
|
|
{
|
|
string AFSToken;
|
|
bool UseAFS = UsingAndroidFileServer(ProjectFile, Configuration, out _, out AFSToken, out _, out _, out _);
|
|
|
|
IProcessResult AdbResult;
|
|
if (UseAFS)
|
|
{
|
|
AdbResult = RunAFSDeviceCommand(string.Format("-p \"{0}\" -k \"{1}\" rm \"{2}\"", Package, AFSToken, DestPath));
|
|
}
|
|
else
|
|
{
|
|
AdbResult = RunAdbDeviceCommand(string.Format("shell rm -f {0}", DestPath));
|
|
}
|
|
return AdbResult.ExitCode == 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a list of locally connected devices (e.g. 'adb devices').
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private static Dictionary<string, string> GetAllConnectedDevices()
|
|
{
|
|
var Result = RunAdbGlobalCommand("devices");
|
|
|
|
MatchCollection DeviceMatches = Regex.Matches(Result.Output, @"^([\d\w\.\:\-]{6,32})\s+(\w+)", RegexOptions.Multiline);
|
|
|
|
var DeviceList = DeviceMatches.Cast<Match>().ToDictionary(
|
|
M => M.Groups[1].ToString(),
|
|
M => M.Groups[2].ToString().ToLower()
|
|
);
|
|
|
|
return DeviceList;
|
|
}
|
|
|
|
private static IEnumerable<string> GetAllAvailableDevices()
|
|
{
|
|
var AllDevices = GetAllConnectedDevices();
|
|
return AllDevices.Keys.Where(D => AllDevices[D] == "device");
|
|
}
|
|
|
|
#region Legacy Implementations
|
|
public IAppInstall InstallApplication(UnrealAppConfig AppConfig)
|
|
{
|
|
// todo - pass this through
|
|
AndroidBuild Build = AppConfig.Build as AndroidBuild;
|
|
|
|
// Ensure APK exists
|
|
if (Build == null)
|
|
{
|
|
throw new AutomationException("Invalid build for Android!");
|
|
}
|
|
|
|
bool bAFSEnablePlugin;
|
|
string AFSToken = "";
|
|
bool bIsShipping;
|
|
bool bAFSIncludeInShipping;
|
|
bool bAFSAllowExternalStartInShipping;
|
|
bool useADB = true;
|
|
if (AppConfig != null)
|
|
{
|
|
if (UsingAndroidFileServer(AppConfig.ProjectFile, Build.Configuration, out bAFSEnablePlugin, out AFSToken, out bIsShipping, out bAFSIncludeInShipping, out bAFSAllowExternalStartInShipping))
|
|
{
|
|
useADB = false;
|
|
}
|
|
}
|
|
|
|
// kill any currently running instance:
|
|
KillRunningProcess(Build.AndroidPackageName);
|
|
|
|
// "/mnt/sdcard";
|
|
// remote dir used to save things
|
|
string ExternalStoragePath = StoragePath + "/UnrealGame/" + AppConfig.ProjectName;
|
|
string ExternalFilesPath = StoragePath + "/Android/data/" + Build.AndroidPackageName + "/files/UnrealGame/" + AppConfig.ProjectName;
|
|
string DeviceBaseDir = Build.UsesExternalFilesDir ? ExternalFilesPath : ExternalStoragePath;
|
|
|
|
// path to the APK to install.
|
|
string ApkPath = Build.SourceApkPath;
|
|
|
|
// get the device's external file paths, always clear between runs
|
|
DeviceExternalStorageSavedPath = string.Format("{0}/{1}/Saved", ExternalStoragePath, AppConfig.ProjectName);
|
|
DeviceExternalFilesSavedPath = string.Format("{0}/{1}/Saved", ExternalFilesPath, AppConfig.ProjectName);
|
|
DeviceLogPath = string.Format("{0}/Logs/{1}.log", Build.UsesPublicLogs ? DeviceExternalFilesSavedPath : DeviceExternalStorageSavedPath, AppConfig.ProjectName);
|
|
DeviceArtifactPath = Build.UsesExternalFilesDir ? DeviceExternalFilesSavedPath : DeviceExternalStorageSavedPath;
|
|
|
|
// path for OBB files
|
|
string OBBRemoteDestination = string.Format("{0}/Android/obb/{1}", StoragePath, Build.AndroidPackageName);
|
|
|
|
Log.Info("DeviceBaseDir: " + DeviceBaseDir);
|
|
Log.Info("DeviceExternalStorageSavedPath: " + DeviceExternalStorageSavedPath);
|
|
Log.Info("DeviceExternalFilesSavedPath: " + DeviceExternalFilesSavedPath);
|
|
Log.Info("DeviceLogPath: " + DeviceLogPath);
|
|
Log.Info("DeviceArtifactPath: " + DeviceArtifactPath);
|
|
|
|
// clear all file store paths between installs:
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}", DeviceExternalStorageSavedPath));
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}", DeviceExternalFilesSavedPath));
|
|
|
|
if (AppConfig.FullClean)
|
|
{
|
|
Log.Info("Fully cleaning console before install...");
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}/UnrealGame/*", StoragePath));
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}/Android/data/{1}/files/*", StoragePath, Build.AndroidPackageName));
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}/obb/{1}/*", StoragePath, Build.AndroidPackageName));
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}/Android/obb/{1}/*", StoragePath, Build.AndroidPackageName));
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}/Download/*", StoragePath));
|
|
}
|
|
|
|
if (!AppConfig.SkipInstall)
|
|
{
|
|
if (Globals.Params.ParseParam("cleandevice")
|
|
|| AppConfig.FullClean)
|
|
{
|
|
Log.Info("Cleaning previous builds due to presence of -cleandevice");
|
|
|
|
// we need to ununstall then install the apk - don't care if it fails, may have been deleted
|
|
Log.Info("Uninstalling {0}", Build.AndroidPackageName);
|
|
RunAdbDeviceCommand(string.Format("uninstall {0}", Build.AndroidPackageName));
|
|
|
|
// delete DeviceExternalStorageSavedPath, note: DeviceExternalFilesSavedPath is removed with package uninstall.
|
|
Log.Info("Removing {0}", DeviceExternalStorageSavedPath);
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}", DeviceExternalStorageSavedPath));
|
|
|
|
Log.Info("Removing {0}", OBBRemoteDestination);
|
|
RunAdbDeviceCommand(string.Format("shell rm -r {0}", OBBRemoteDestination));
|
|
}
|
|
|
|
// check for a local newer executable
|
|
if (Globals.Params.ParseParam("dev"))
|
|
{
|
|
//string ApkFileName = Path.GetFileName(ApkPath);
|
|
|
|
string ApkFileName2 = UnrealHelpers.GetExecutableName(AppConfig.ProjectName, UnrealTargetPlatform.Android, AppConfig.Configuration, AppConfig.ProcessType, "apk");
|
|
|
|
string LocalAPK = Path.Combine(Environment.CurrentDirectory, AppConfig.ProjectName, "Binaries/Android", ApkFileName2);
|
|
|
|
bool LocalFileExists = File.Exists(LocalAPK);
|
|
bool LocalFileNewer = LocalFileExists && File.GetLastWriteTime(LocalAPK) > File.GetLastWriteTime(ApkPath);
|
|
|
|
Log.Verbose("Checking for newer binary at {0}", LocalAPK);
|
|
Log.Verbose("LocalFile exists: {0}. Newer: {1}", LocalFileExists, LocalFileNewer);
|
|
|
|
if (LocalFileExists && LocalFileNewer)
|
|
{
|
|
ApkPath = LocalAPK;
|
|
}
|
|
}
|
|
|
|
bool NoPlayProtect = Globals.Params.ParseParam("no-play-protect");
|
|
if (NoPlayProtect)
|
|
{
|
|
NoPlayProtectSetting = RunAdbDeviceCommandAndGetOutput("shell settings get global package_verifier_user_consent");
|
|
if (NoPlayProtectSetting != "-1")
|
|
{
|
|
Log.Verbose("Removing play protect for this session...");
|
|
RunAdbDeviceCommand("shell settings put global package_verifier_user_consent -1");
|
|
}
|
|
}
|
|
|
|
// first install the APK
|
|
CopyFileToDevice(Build.AndroidPackageName, ApkPath, string.Empty, AppConfig.ProjectFile, AppConfig.Configuration);
|
|
|
|
// remote dir on the device, create it if it doesn't exist
|
|
if (useADB)
|
|
{
|
|
RunAdbDeviceCommand(string.Format("shell mkdir -p {0}/", DeviceExternalStorageSavedPath));
|
|
RunAdbDeviceCommand(string.Format("shell mkdir -p {0}/", DeviceExternalFilesSavedPath));
|
|
}
|
|
else
|
|
{
|
|
RunAFSDeviceCommand(string.Format("-p {0} -k {1} mkdir \"^saved\"", Build.AndroidPackageName, AFSToken));
|
|
}
|
|
}
|
|
|
|
// Convert the files from the source to final destination names
|
|
Dictionary<string, string> FilesToInstall = new Dictionary<string, string>();
|
|
|
|
Console.WriteLine("trying to copy files over.");
|
|
if (AppConfig.FilesToCopy != null)
|
|
{
|
|
if (LocalDirectoryMappings.Count == 0)
|
|
{
|
|
Console.WriteLine("Populating Directory");
|
|
PopulateDirectoryMappings(Path.GetDirectoryName(DeviceArtifactPath));
|
|
}
|
|
foreach (UnrealFileToCopy FileToCopy in AppConfig.FilesToCopy)
|
|
{
|
|
string PathToCopyTo = Path.Combine(LocalDirectoryMappings[FileToCopy.TargetBaseDirectory], FileToCopy.TargetRelativeLocation);
|
|
if (File.Exists(FileToCopy.SourceFileLocation))
|
|
{
|
|
FileInfo SrcInfo = new FileInfo(FileToCopy.SourceFileLocation);
|
|
SrcInfo.IsReadOnly = false;
|
|
FilesToInstall.Add(FileToCopy.SourceFileLocation, PathToCopyTo.Replace("\\", "/"));
|
|
}
|
|
|
|
else
|
|
{
|
|
Log.Warning(KnownLogEvents.Gauntlet_DeviceEvent, "File to copy {File} not found", FileToCopy);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!AppConfig.SkipInstall)
|
|
{
|
|
// obb files need to be named based on APK version (grrr), so find that out. This should return something like
|
|
// versionCode=2 minSdk=21 targetSdk=21
|
|
string PackageInfo = RunAdbDeviceCommand(string.Format("shell dumpsys package {0} | grep versionCode", Build.AndroidPackageName)).Output;
|
|
var Match = Regex.Match(PackageInfo, @"versionCode=([\d\.]+)\s");
|
|
if (Match.Success == false)
|
|
{
|
|
throw new AutomationException("Failed to find version info for APK!");
|
|
}
|
|
string PackageVersion = Match.Groups[1].ToString();
|
|
|
|
Build.FilesToInstall.Keys.ToList().ForEach(K =>
|
|
{
|
|
string SrcPath = K;
|
|
string SrcFile = Path.GetFileName(SrcPath);
|
|
|
|
string DestPath = Build.FilesToInstall[K];
|
|
string DestFile = Path.GetFileName(DestPath);
|
|
|
|
// If we installed a new APK we need to change the package version
|
|
Match OBBMatch = Regex.Match(SrcFile, @"\.(\d+)\.com.*\.obb");
|
|
if (OBBMatch.Success)
|
|
{
|
|
DestPath = StoragePath + "/Android/obb/" + Build.AndroidPackageName + "/" + SrcFile.Replace(".Client.obb", ".obb").Replace(OBBMatch.Groups[1].ToString(), PackageVersion);
|
|
}
|
|
|
|
DestPath = Regex.Replace(DestPath, "%STORAGE%", StoragePath, RegexOptions.IgnoreCase);
|
|
|
|
FilesToInstall.Add(SrcPath, DestPath);
|
|
});
|
|
|
|
// get a list of files in the destination OBB directory
|
|
IProcessResult AdbResult;
|
|
AdbResult = RunAdbDeviceCommand(string.Format("shell ls {0}", OBBRemoteDestination));
|
|
|
|
// if != 0 then no folder exists
|
|
if (AdbResult.ExitCode == 0)
|
|
{
|
|
string[] Delimiters = { "\r\n", "\n" };
|
|
string[] CurrentRemoteFileList = AdbResult.Output.Split(Delimiters, StringSplitOptions.RemoveEmptyEntries);
|
|
for (int i = 0; i < CurrentRemoteFileList.Length; ++i)
|
|
{
|
|
CurrentRemoteFileList[i] = CurrentRemoteFileList[i].Trim();
|
|
}
|
|
|
|
IEnumerable<string> NewRemoteFileList = FilesToInstall.Values.Select(F => Path.GetFileName(F));
|
|
|
|
// delete any files that should not be there
|
|
foreach (string FileName in CurrentRemoteFileList)
|
|
{
|
|
if (FileName.StartsWith(".") || FileName.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (NewRemoteFileList.Contains(FileName) == false)
|
|
{
|
|
RunAdbDeviceCommand(string.Format("shell rm {0}/{1}", OBBRemoteDestination, FileName));
|
|
}
|
|
}
|
|
}
|
|
|
|
EnablePermissions(Build.AndroidPackageName);
|
|
|
|
// Copy other file dependencies (including OBB files)
|
|
foreach (var KV in FilesToInstall)
|
|
{
|
|
string LocalFile = KV.Key;
|
|
string RemoteFile = KV.Value;
|
|
|
|
Console.WriteLine("Copying {0} to {1}", LocalFile, RemoteFile);
|
|
if (useADB)
|
|
{
|
|
CopyFileToDevice(Build.AndroidPackageName, LocalFile, RemoteFile, AppConfig.ProjectFile, AppConfig.Configuration);
|
|
}
|
|
else
|
|
{
|
|
string Filename = Path.GetFileName(LocalFile);
|
|
string[] parts = Filename.ToString().Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries).Select(S => S.Trim()).ToArray();
|
|
// Make sure last part is obb
|
|
if (parts[parts.Length - 1] == "obb")
|
|
{
|
|
string obbname = string.Format("^{0}obb", parts[0]);
|
|
CopyFileToDevice(Build.AndroidPackageName, LocalFile, obbname, AppConfig.ProjectFile, AppConfig.Configuration);
|
|
}
|
|
else
|
|
{
|
|
CopyFileToDevice(Build.AndroidPackageName, LocalFile, RemoteFile, AppConfig.ProjectFile, AppConfig.Configuration);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log.Info("Skipping install of {0} (-skipdeploy)", Build.AndroidPackageName);
|
|
}
|
|
|
|
CopyCommandlineFile(AppConfig, AppConfig.CommandLine, Build.AndroidPackageName, AppConfig.ProjectName, Build.UsesExternalFilesDir);
|
|
|
|
AndroidAppInstall AppInstall = new AndroidAppInstall(AppConfig, this, InApkPath: ApkPath, AppConfig.ProjectName, Build.AndroidPackageName, AppConfig.CommandLine, AppConfig.ProjectFile, Build.Configuration);
|
|
|
|
return AppInstall;
|
|
}
|
|
#endregion
|
|
}
|
|
|
|
class AndroidAppInstall : IAppInstall
|
|
{
|
|
public string Name { get; protected set; }
|
|
|
|
public TargetDeviceAndroid AndroidDevice { get; protected set; }
|
|
|
|
public ITargetDevice Device => AndroidDevice;
|
|
|
|
public string AndroidPackageName { get; protected set; }
|
|
|
|
public string CommandLine { get; protected set; }
|
|
|
|
public string AppTag { get; set; }
|
|
|
|
public string ApkPath { get; protected set; }
|
|
|
|
public FileReference ProjectFile { get; protected set; }
|
|
|
|
public UnrealTargetConfiguration Configuration { get; protected set; }
|
|
|
|
public UnrealAppConfig AppConfig { get; protected set; }
|
|
|
|
public AndroidAppInstall(UnrealAppConfig InAppConfig, TargetDeviceAndroid InDevice, string InName, string InAndroidPackageName, string InCommandLine, FileReference InProjectFile, UnrealTargetConfiguration InConfiguration, string InAppTag = "UE")
|
|
{
|
|
AppConfig = InAppConfig;
|
|
AndroidDevice = InDevice;
|
|
Name = InName;
|
|
AndroidPackageName = InAndroidPackageName;
|
|
CommandLine = InCommandLine;
|
|
AppTag = InAppTag;
|
|
ProjectFile = InProjectFile;
|
|
Configuration = InConfiguration;
|
|
}
|
|
|
|
public AndroidAppInstall(UnrealAppConfig InAppConfig, TargetDeviceAndroid InDevice, string InApkPath, string InName, string InAndroidPackageName, string InCommandLine, FileReference InProjectFile, UnrealTargetConfiguration InConfiguration, string InAppTag = "UE")
|
|
{
|
|
AppConfig = InAppConfig;
|
|
AndroidDevice = InDevice;
|
|
ApkPath = InApkPath;
|
|
Name = InName;
|
|
AndroidPackageName = InAndroidPackageName;
|
|
CommandLine = InCommandLine;
|
|
AppTag = InAppTag;
|
|
ProjectFile = InProjectFile;
|
|
Configuration = InConfiguration;
|
|
}
|
|
|
|
public IAppInstance Run()
|
|
{
|
|
return AndroidDevice.Run(this);
|
|
}
|
|
}
|
|
|
|
public class DefaultAndroidDevices : IDefaultDeviceSource
|
|
{
|
|
public bool CanSupportPlatform(UnrealTargetPlatform? Platform)
|
|
{
|
|
return Platform == UnrealTargetPlatform.Android;
|
|
}
|
|
|
|
public ITargetDevice[] GetDefaultDevices()
|
|
{
|
|
return TargetDeviceAndroid.GetDefaultDevices();
|
|
}
|
|
}
|
|
|
|
// become IAppInstance when implemented enough
|
|
class AndroidAppInstance : IAppInstance
|
|
{
|
|
public string ArtifactPath
|
|
{
|
|
get
|
|
{
|
|
if (HasExited && !bHaveSavedArtifacts)
|
|
{
|
|
bHaveSavedArtifacts = true;
|
|
SaveArtifacts();
|
|
}
|
|
|
|
return Path.Combine(AndroidDevice.LocalCachePath, "Saved");
|
|
}
|
|
}
|
|
|
|
public bool HasExited
|
|
{
|
|
get
|
|
{
|
|
try
|
|
{
|
|
if (!LaunchProcess.HasExited)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return !IsActivityRunning();
|
|
}
|
|
}
|
|
|
|
public string StdOut
|
|
{
|
|
get
|
|
{
|
|
return LogProcess.Output;
|
|
}
|
|
}
|
|
|
|
public ILogStreamReader GetLogReader() => LogProcess.GetLogReader();
|
|
|
|
public ILogStreamReader GetLogBufferReader() => LogProcess.GetLogBufferReader();
|
|
|
|
public bool WriteOutputToFile(string FilePath) => LogProcess.WriteOutputToFile(FilePath) != null;
|
|
|
|
public string CommandLine => Install.CommandLine;
|
|
|
|
public ITargetDevice Device => AndroidDevice;
|
|
|
|
public int ExitCode => LaunchProcess.ExitCode;
|
|
|
|
public bool WasKilled { get; protected set; }
|
|
|
|
public IProcessResult LaunchProcess;
|
|
|
|
protected ILongProcessResult LogProcess;
|
|
|
|
protected TargetDeviceAndroid AndroidDevice;
|
|
|
|
protected AndroidAppInstall Install;
|
|
|
|
protected bool bHaveSavedArtifacts;
|
|
|
|
private bool ActivityExited = false;
|
|
|
|
public AdbScreenRecorder Recorder;
|
|
|
|
public AndroidAppInstance(TargetDeviceAndroid InDevice, AndroidAppInstall InInstall, IProcessResult InProcess)
|
|
{
|
|
AndroidDevice = InDevice;
|
|
Install = InInstall;
|
|
LaunchProcess = InProcess;
|
|
LogProcess = AndroidDevice.RunAdbCommandNoWait($"logcat -s {Install.AppTag} debug Debug DEBUG -v raw", Install.Device.LocalCachePath);
|
|
|
|
if (Globals.Params.ParseParam("screenrecord"))
|
|
{
|
|
StartRecording();
|
|
}
|
|
}
|
|
|
|
public int WaitForExit()
|
|
{
|
|
if (!HasExited)
|
|
{
|
|
LaunchProcess.WaitForExit();
|
|
LogProcess.StopProcess();
|
|
}
|
|
|
|
return ExitCode;
|
|
}
|
|
|
|
public void Kill(bool GenerateDump)
|
|
{
|
|
if (!HasExited && !AndroidDevice.Disposed)
|
|
{
|
|
WasKilled = true;
|
|
Install.AndroidDevice.KillRunningProcess(Install.AndroidPackageName);
|
|
LogProcess.StopProcess();
|
|
StopRecording();
|
|
}
|
|
}
|
|
public void StartRecording()
|
|
{
|
|
// Ensure artifact directories exist
|
|
if (TargetDeviceAndroid.UsingAndroidFileServer(Install.ProjectFile, Install.Configuration, out _, out string AFSToken, out _, out _, out _))
|
|
{
|
|
Install.AndroidDevice.RunAFSDeviceCommand(string.Format("-p \"{0}\" -k \"{1}\" mkdir \"^saved/Logs\"", Install.AndroidPackageName, AFSToken));
|
|
}
|
|
else
|
|
{
|
|
Install.AndroidDevice.RunAdbDeviceCommand($"shell mkdir -p {Install.AndroidDevice.DeviceArtifactPath}/Logs/");
|
|
}
|
|
Recorder = new AdbScreenRecorder();
|
|
Recorder.StartRecording(Install.AndroidDevice.DeviceName, $"{Install.AndroidDevice.DeviceArtifactPath}/Logs/screen_recording.mp4");
|
|
}
|
|
|
|
public void StopRecording()
|
|
{
|
|
if (Recorder != null)
|
|
{
|
|
Recorder.StopRecording();
|
|
Recorder.Dispose();
|
|
Recorder = null;
|
|
}
|
|
}
|
|
|
|
protected void SaveArtifacts()
|
|
{
|
|
StopRecording();
|
|
|
|
// Pull all the artifacts
|
|
string ArtifactPullCommand = string.Format("pull {0} \"{1}\"", Install.AndroidDevice.DeviceArtifactPath, Install.AndroidDevice.LocalCachePath);
|
|
IProcessResult PullCmd = Install.AndroidDevice.RunAdbDeviceCommand(ArtifactPullCommand, bShouldLogCommand: Log.IsVerbose);
|
|
|
|
if (PullCmd.ExitCode != 0)
|
|
{
|
|
Log.Warning(KnownLogEvents.Gauntlet_DeviceEvent, "Failed to retrieve artifacts. {Output}", PullCmd.Output);
|
|
}
|
|
|
|
// pull the logcat over from device.
|
|
IProcessResult LogcatResult = Install.AndroidDevice.RunAdbDeviceCommand("logcat -d", bShouldLogCommand: Log.IsVerbose);
|
|
|
|
string SavedDirectory = Path.Combine(Install.AndroidDevice.LocalCachePath, "Saved");
|
|
string LogcatFilename = "Logcat.log";
|
|
// Save logcat dump to local artifact path.
|
|
if (!Directory.Exists(SavedDirectory))
|
|
{
|
|
Directory.CreateDirectory(SavedDirectory);
|
|
}
|
|
File.WriteAllText(Path.Combine(SavedDirectory, LogcatFilename), LogcatResult.Output);
|
|
bHaveSavedArtifacts = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks on device whether the activity is running
|
|
/// </summary>
|
|
private bool IsActivityRunning()
|
|
{
|
|
if (ActivityExited || AndroidDevice.Disposed)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// get activities filtered by our package name
|
|
IProcessResult ActivityQuery = AndroidDevice.RunAdbDeviceCommand("shell dumpsys activity -p " + Install.AndroidPackageName + " a");
|
|
|
|
// We have exited if our activity doesn't appear in the activity query or is not the focused activity.
|
|
bool bActivityPresent = ActivityQuery.Output.Contains(Install.AndroidPackageName);
|
|
bool bActivityInForeground = ActivityQuery.Output.Contains("ResumedActivity");
|
|
bool bHasExited = !bActivityPresent || !bActivityInForeground;
|
|
if (bHasExited)
|
|
{
|
|
ActivityExited = true;
|
|
if (!LogProcess.HasExited)
|
|
{
|
|
// The activity has exited, make sure entire activity log has been captured, sleep to allow time for the log to flush
|
|
Thread.Sleep(5000);
|
|
LogProcess.StopProcess();
|
|
}
|
|
Log.VeryVerbose("{0}: process exited, Activity running={1}, Activity in foreground={2} ", ToString(), bActivityPresent.ToString(), bActivityInForeground.ToString());
|
|
}
|
|
|
|
return !bHasExited;
|
|
}
|
|
}
|
|
|
|
public class AdbScreenRecorder : IDisposable
|
|
{
|
|
private Process ADBProcess;
|
|
private bool Disposed;
|
|
|
|
public void StartRecording(string Device, string OutputFilePath)
|
|
{
|
|
if (ADBProcess != null && !ADBProcess.HasExited)
|
|
{
|
|
throw new InvalidOperationException("A screen recording session is already in progress.");
|
|
}
|
|
|
|
string AdbCommand = Environment.ExpandEnvironmentVariables("%ANDROID_HOME%/platform-tools/adb" + (OperatingSystem.IsWindows() ? ".exe" : ""));
|
|
|
|
ADBProcess = new Process
|
|
{
|
|
StartInfo = new ProcessStartInfo
|
|
{
|
|
FileName = AdbCommand,
|
|
Arguments = $"-s {Device} shell screenrecord {OutputFilePath}",
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardInput = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
}
|
|
};
|
|
|
|
ADBProcess.Start();
|
|
}
|
|
public void StopRecording(int timeout = 500)
|
|
{
|
|
if (ADBProcess != null && !ADBProcess.HasExited)
|
|
{
|
|
try
|
|
{
|
|
ADBProcess.StandardInput.WriteLine("\x03"); // Send Ctrl+C to stop the recording
|
|
if (!ADBProcess.WaitForExit(timeout))
|
|
{
|
|
// Process did not exit within the specified timeout
|
|
KillAdbProcess();
|
|
}
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// Handle the exception if StandardInput is not available
|
|
KillAdbProcess();
|
|
}
|
|
}
|
|
}
|
|
private void KillAdbProcess()
|
|
{
|
|
try
|
|
{
|
|
if (ADBProcess != null && !ADBProcess.HasExited)
|
|
{
|
|
ADBProcess.Kill();
|
|
}
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// Process has already exited, do nothing
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (!Disposed)
|
|
{
|
|
if (disposing)
|
|
{
|
|
// Release managed resources
|
|
StopRecording();
|
|
}
|
|
|
|
// Release unmanaged resources
|
|
if (ADBProcess != null)
|
|
{
|
|
ADBProcess.Dispose();
|
|
ADBProcess = null;
|
|
}
|
|
|
|
Disposed = true;
|
|
}
|
|
}
|
|
|
|
~AdbScreenRecorder()
|
|
{
|
|
Dispose(false);
|
|
}
|
|
}
|
|
|
|
// Device data from json
|
|
public sealed class AndroidDeviceData
|
|
{
|
|
// Host of PC which is tethered
|
|
public string HostIP { get; set; }
|
|
|
|
// public key
|
|
public string PublicKey { get; set; }
|
|
|
|
// private key
|
|
public string PrivateKey { get; set; }
|
|
}
|
|
|
|
public class AndroidBuildSupport : BaseBuildSupport
|
|
{
|
|
protected override BuildFlags SupportedBuildTypes => BuildFlags.Packaged | BuildFlags.CanReplaceCommandLine | BuildFlags.CanReplaceExecutable | BuildFlags.Bulk | BuildFlags.NotBulk;
|
|
protected override UnrealTargetPlatform? Platform => UnrealTargetPlatform.Android;
|
|
}
|
|
|
|
public class AndroidDeviceFactory : IDeviceFactory
|
|
{
|
|
public bool CanSupportPlatform(UnrealTargetPlatform? Platform)
|
|
{
|
|
return Platform == UnrealTargetPlatform.Android;
|
|
}
|
|
|
|
public ITargetDevice CreateDevice(string InRef, string InCachePath, string InParam = null)
|
|
{
|
|
AndroidDeviceData DeviceData = null;
|
|
|
|
if (!string.IsNullOrEmpty(InParam))
|
|
{
|
|
DeviceData = fastJSON.JSON.Instance.ToObject<AndroidDeviceData>(InParam);
|
|
}
|
|
|
|
return new TargetDeviceAndroid(InRef, DeviceData, InCachePath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// ADB key credentials, running adb-server commands (must) use same pub/private key store
|
|
/// </summary>
|
|
internal static class AdbCredentialCache
|
|
{
|
|
private static int InstanceCount = 0;
|
|
private static bool bUsingCustomKeys = false;
|
|
|
|
private static string PrivateKey;
|
|
private static string PublicKey;
|
|
|
|
private const string KeyBackupExt = ".gauntlet.bak";
|
|
|
|
static AdbCredentialCache()
|
|
{
|
|
Reset();
|
|
}
|
|
|
|
public static void RemoveInstance()
|
|
{
|
|
lock (Globals.MainLock)
|
|
{
|
|
InstanceCount--;
|
|
|
|
if (InstanceCount == 0 && bUsingCustomKeys)
|
|
{
|
|
Reset();
|
|
KillAdbServer();
|
|
|
|
// Kill ADB server, just as a safety measure to ensure it closes
|
|
IEnumerable<Process> ADBProcesses = Process.GetProcesses().Where(p => p.ProcessName.Equals("adb"));
|
|
if (ADBProcesses.Count() > 0)
|
|
{
|
|
Log.Info("Terminating {0} ADB Process(es)", ADBProcesses.Count());
|
|
foreach (Process ADBProcess in ADBProcesses)
|
|
{
|
|
Log.Info("Killing ADB process {0}", ADBProcess.Id);
|
|
ADBProcess.Kill();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void RestoreBackupKeys()
|
|
{
|
|
string LocalKeyPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".android");
|
|
string LocalKeyFile = Path.Combine(LocalKeyPath, "adbkey");
|
|
string LocalPubKeyFile = Path.Combine(LocalKeyPath, "adbkey.pub");
|
|
string BackupSentry = Path.Combine(LocalKeyPath, "gauntlet.inuse");
|
|
|
|
if (File.Exists(BackupSentry))
|
|
{
|
|
Log.Info("Restoring original adb keys");
|
|
|
|
if (File.Exists(LocalKeyFile + KeyBackupExt))
|
|
{
|
|
File.Copy(LocalKeyFile + KeyBackupExt, LocalKeyFile, true);
|
|
File.Delete(LocalKeyFile + KeyBackupExt);
|
|
}
|
|
else
|
|
{
|
|
File.Delete(LocalKeyFile);
|
|
}
|
|
|
|
if (File.Exists(LocalPubKeyFile + KeyBackupExt))
|
|
{
|
|
File.Copy(LocalPubKeyFile + KeyBackupExt, LocalPubKeyFile, true);
|
|
File.Delete(LocalPubKeyFile + KeyBackupExt);
|
|
}
|
|
else
|
|
{
|
|
File.Delete(LocalPubKeyFile);
|
|
}
|
|
|
|
File.Delete(BackupSentry);
|
|
}
|
|
|
|
}
|
|
|
|
public static void AddInstance(AndroidDeviceData DeviceData = null)
|
|
{
|
|
lock (Globals.MainLock)
|
|
{
|
|
string KeyPath = Globals.Params.ParseValue("adbkeys", null);
|
|
|
|
// setup key store from device data
|
|
if (string.IsNullOrEmpty(KeyPath) && DeviceData != null)
|
|
{
|
|
// checked that cached keys are the same
|
|
if (!string.IsNullOrEmpty(PrivateKey))
|
|
{
|
|
if (PrivateKey != DeviceData.PrivateKey)
|
|
{
|
|
throw new AutomationException("ADB device private keys must match");
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(PublicKey))
|
|
{
|
|
if (PublicKey != DeviceData.PublicKey)
|
|
{
|
|
throw new AutomationException("ADB device public keys must match");
|
|
}
|
|
}
|
|
|
|
PrivateKey = DeviceData.PrivateKey;
|
|
PublicKey = DeviceData.PublicKey;
|
|
|
|
if (string.IsNullOrEmpty(PublicKey) || string.IsNullOrEmpty(PrivateKey))
|
|
{
|
|
throw new AutomationException("Invalid key in device data");
|
|
}
|
|
|
|
KeyPath = Path.Combine(Globals.TempDir, "AndroidADBKeys");
|
|
|
|
if (!Directory.Exists(KeyPath))
|
|
{
|
|
Directory.CreateDirectory(KeyPath);
|
|
}
|
|
|
|
if (InstanceCount == 0)
|
|
{
|
|
byte[] data = Convert.FromBase64String(PrivateKey);
|
|
File.WriteAllText(KeyPath + "/adbkey", Encoding.UTF8.GetString(data));
|
|
|
|
data = Convert.FromBase64String(PublicKey);
|
|
File.WriteAllText(KeyPath + "/adbkey.pub", Encoding.UTF8.GetString(data));
|
|
}
|
|
|
|
}
|
|
|
|
if (InstanceCount == 0 && !String.IsNullOrEmpty(KeyPath))
|
|
{
|
|
|
|
Log.Info("Using adb keys at {0}", KeyPath);
|
|
|
|
string LocalKeyPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".android");
|
|
if(!Directory.Exists(LocalKeyPath))
|
|
{
|
|
Directory.CreateDirectory(LocalKeyPath);
|
|
}
|
|
|
|
string RemoteKeyFile = Path.Combine(KeyPath, "adbkey");
|
|
string RemotePubKeyFile = Path.Combine(KeyPath, "adbkey.pub");
|
|
string LocalKeyFile = Path.Combine(LocalKeyPath, "adbkey");
|
|
string LocalPubKeyFile = Path.Combine(LocalKeyPath, "adbkey.pub");
|
|
string BackupSentry = Path.Combine(LocalKeyPath, "gauntlet.inuse");
|
|
|
|
if (File.Exists(RemoteKeyFile) == false)
|
|
{
|
|
throw new AutomationException("adbkey at {0} does not exist", KeyPath);
|
|
}
|
|
|
|
if (File.Exists(RemotePubKeyFile) == false)
|
|
{
|
|
throw new AutomationException("adbkey.pub at {0} does not exist", KeyPath);
|
|
}
|
|
|
|
if (File.Exists(BackupSentry) == false)
|
|
{
|
|
if (File.Exists(LocalKeyFile))
|
|
{
|
|
File.Copy(LocalKeyFile, LocalKeyFile + KeyBackupExt, true);
|
|
}
|
|
|
|
if (File.Exists(LocalPubKeyFile))
|
|
{
|
|
File.Copy(LocalPubKeyFile, LocalPubKeyFile + KeyBackupExt, true);
|
|
}
|
|
File.WriteAllText(BackupSentry, "placeholder");
|
|
}
|
|
|
|
File.Copy(RemoteKeyFile, LocalKeyFile, true);
|
|
File.Copy(RemotePubKeyFile, LocalPubKeyFile, true);
|
|
|
|
bUsingCustomKeys = true;
|
|
|
|
KillAdbServer();
|
|
}
|
|
|
|
InstanceCount++;
|
|
}
|
|
|
|
}
|
|
|
|
private static void Reset()
|
|
{
|
|
if (InstanceCount != 0)
|
|
{
|
|
throw new AutomationException("AdbCredentialCache.Reset() called with outstanding instances");
|
|
}
|
|
|
|
PrivateKey = PublicKey = string.Empty;
|
|
bUsingCustomKeys = false;
|
|
|
|
RestoreBackupKeys();
|
|
}
|
|
|
|
private static void KillAdbServer()
|
|
{
|
|
using (new ScopedSuspendECErrorParsing())
|
|
{
|
|
Log.Info("Running adb kill-server to refresh credentials");
|
|
TargetDeviceAndroid.RunAdbGlobalCommand("kill-server");
|
|
|
|
// Killing the adb server restarts it and can surface superfluous device errors
|
|
int SleepTime = CommandUtils.IsBuildMachine ? 15000 : 5000;
|
|
Thread.Sleep(SleepTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
class AndroidPlatformSupport : TargetPlatformSupportBase
|
|
{
|
|
public override UnrealTargetPlatform? Platform => UnrealTargetPlatform.Android;
|
|
public override bool IsHostMountingSupported() => false;
|
|
}
|
|
} |