// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= AndroidTargetPlatform.inl: Implements the FAndroidTargetPlatformControls class. =============================================================================*/ /* FAndroidTargetPlatformControls structors *****************************************************************************/ #include "AndroidTargetPlatformControls.h" #include "AndroidTargetPlatformSettings.h" #include "CoreTypes.h" #include "Misc/AssertionMacros.h" #include "Containers/Array.h" #include "Containers/UnrealString.h" #include "UObject/NameTypes.h" #include "Logging/LogMacros.h" #include "Stats/Stats.h" #include "Serialization/Archive.h" #include "Misc/FileHelper.h" #include "Misc/SecureHash.h" #include "HAL/FileManager.h" #include "HAL/PlatformFileManager.h" #include "HAL/IConsoleManager.h" #include "Interfaces/IAndroidDeviceDetectionModule.h" #include "Interfaces/IAndroidDeviceDetection.h" #include "Modules/ModuleManager.h" #include "Misc/SecureHash.h" #include "AnalyticsEventAttribute.h" #if WITH_ENGINE #include "AudioCompressionSettings.h" #include "Sound/SoundWave.h" #include "TextureResource.h" #endif #define LOCTEXT_NAMESPACE "FAndroidTargetPlatformControls" class Error; class FAndroidTargetDevice; class FConfigCacheIni; class FModuleManager; class FScopeLock; class FStaticMeshLODSettings; class FTargetDeviceId; class FTSTicker; class IAndroidDeviceDetectionModule; class UTexture; class UTextureLODSettings; struct FAndroidDeviceInfo; enum class ETargetPlatformFeatures; static FString GetLicensePath() { auto &AndroidDeviceDetection = FModuleManager::LoadModuleChecked("AndroidDeviceDetection"); IAndroidDeviceDetection* DeviceDetection = AndroidDeviceDetection.GetAndroidDeviceDetection(); FString ADBPath = DeviceDetection->GetADBPath(); if (!FPaths::FileExists(ADBPath)) { return TEXT(""); } // strip off the adb.exe part FString PlatformToolsPath; FString Filename; FString Extension; FPaths::Split(ADBPath, PlatformToolsPath, Filename, Extension); // remove the platform-tools part and point to licenses FPaths::NormalizeDirectoryName(PlatformToolsPath); FString LicensePath = PlatformToolsPath + "/../licenses"; FPaths::CollapseRelativeDirectories(LicensePath); return LicensePath; } #if WITH_ENGINE static bool GetLicenseHash(FSHAHash& LicenseHash) { bool bLicenseValid = false; // from Android SDK Tools 25.2.3 FString LicenseFilename = FPaths::EngineDir() + TEXT("Source/ThirdParty/Android/package.xml"); // Create file reader TUniquePtr FileReader(IFileManager::Get().CreateFileReader(*LicenseFilename)); if (FileReader) { // Create buffer for file input uint32 BufferSize = IntCastChecked(FileReader->TotalSize()); uint8* Buffer = (uint8*)FMemory::Malloc(BufferSize); FileReader->Serialize(Buffer, BufferSize); uint8 StartPattern[] = ""; int32 StartPatternLength = strlen((char *)StartPattern); uint8* LicenseStart = Buffer; uint8* BufferEnd = Buffer + BufferSize - StartPatternLength; while (LicenseStart < BufferEnd) { if (!memcmp(LicenseStart, StartPattern, StartPatternLength)) { break; } LicenseStart++; } if (LicenseStart < BufferEnd) { LicenseStart += StartPatternLength; uint8 EndPattern[] = ""; int32 EndPatternLength = strlen((char *)EndPattern); uint8* LicenseEnd = LicenseStart; BufferEnd = Buffer + BufferSize - EndPatternLength; while (LicenseEnd < BufferEnd) { if (!memcmp(LicenseEnd, EndPattern, EndPatternLength)) { break; } LicenseEnd++; } if (LicenseEnd < BufferEnd) { int32 LicenseLength = IntCastChecked(LicenseEnd - LicenseStart); FSHA1::HashBuffer(LicenseStart, LicenseLength, LicenseHash.Hash); bLicenseValid = true; } } FMemory::Free(Buffer); } return bLicenseValid; } #endif static bool HasLicense() { #if WITH_ENGINE FString LicensePath = GetLicensePath(); if (LicensePath.IsEmpty()) { return false; } // directory must exist IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); if (!PlatformFile.DirectoryExists(*LicensePath)) { return false; } // license file must exist FString LicenseFilename = LicensePath + "/android-sdk-license"; if (!PlatformFile.FileExists(*LicenseFilename)) { return false; } FSHAHash LicenseHash; if (!GetLicenseHash(LicenseHash)) { return false; } // contents must match hash of license text FString FileData = ""; FFileHelper::LoadFileToString(FileData, *LicenseFilename); TArray lines; int32 lineCount = FileData.ParseIntoArray(lines, TEXT("\n"), true); FString LicenseString = LicenseHash.ToString().ToLower(); for (FString &line : lines) { if (line.TrimStartAndEnd().Equals(LicenseString)) { return true; } } #endif // doesn't match return false; } FAndroidTargetPlatformControls::FAndroidTargetPlatformControls(bool bInIsClient, ITargetPlatformSettings* InTargetPlatformSettings, const TCHAR* FlavorName, const TCHAR* OverrideIniPlatformName) : TNonDesktopTargetPlatformControlsBase(bInIsClient, InTargetPlatformSettings, FlavorName, OverrideIniPlatformName) , DeviceDetection(nullptr) { TickDelegate = FTickerDelegate::CreateRaw(this, &FAndroidTargetPlatformControls::HandleTicker); TickDelegateHandle = FTSTicker::GetCoreTicker().AddTicker(TickDelegate, 4.0f); AndroidTargetPlatformSettings = (FAndroidTargetPlatformSettings*)TargetPlatformSettings; } FAndroidTargetPlatformControls::~FAndroidTargetPlatformControls() { FTSTicker::GetCoreTicker().RemoveTicker(TickDelegateHandle); } /* ITargetPlatform overrides *****************************************************************************/ void FAndroidTargetPlatformControls::GetAllDevices( TArray& OutDevices ) const { OutDevices.Reset(); for (auto Iter = Devices.CreateConstIterator(); Iter; ++Iter) { OutDevices.Add(Iter.Value()); } } ITargetDevicePtr FAndroidTargetPlatformControls::GetDefaultDevice( ) const { // return the first device in the list if (Devices.Num() > 0) { auto Iter = Devices.CreateConstIterator(); if (Iter) { return Iter.Value(); } } return nullptr; } ITargetDevicePtr FAndroidTargetPlatformControls::GetDevice( const FTargetDeviceId& DeviceId ) { if (DeviceId.GetPlatformName() == PlatformName()) { return Devices.FindRef(DeviceId.GetDeviceName()); } return nullptr; } bool FAndroidTargetPlatformControls::IsSdkInstalled(bool bProjectHasCode, FString& OutDocumentationPath) const { OutDocumentationPath = FString("Shared/Tutorials/SettingUpAndroidTutorial"); return true; } int32 FAndroidTargetPlatformControls::CheckRequirements(bool bProjectHasCode, EBuildConfiguration Configuration, bool bRequiresAssetNativization, FString& OutTutorialPath, FString& OutDocumentationPath, FText& CustomizedLogMessage) const { OutDocumentationPath = TEXT("Platforms/Android/GettingStarted"); int32 bReadyToBuild = ETargetPlatformReadyStatus::Ready; if (!IsSdkInstalled(bProjectHasCode, OutTutorialPath)) { bReadyToBuild |= ETargetPlatformReadyStatus::SDKNotFound; } // need to check license was accepted if (!HasLicense()) { OutTutorialPath.Empty(); CustomizedLogMessage = LOCTEXT("AndroidLicenseNotAcceptedMessageDetail", "SDK License must be accepted in the Android project settings to deploy your app to the device."); bReadyToBuild |= ETargetPlatformReadyStatus::LicenseNotAccepted; } return bReadyToBuild; } void FAndroidTargetPlatformControls::GetPlatformSpecificProjectAnalytics(TArray& AnalyticsParamArray) const { TTargetPlatformControlsBase::GetPlatformSpecificProjectAnalytics(AnalyticsParamArray); AppendAnalyticsEventAttributeArray(AnalyticsParamArray, TEXT("AndroidVariant"), GetAndroidVariantName(), TEXT("SupportsVulkan"), AndroidTargetPlatformSettings->SupportsVulkan(), TEXT("SupportsVulkanSM5"), AndroidTargetPlatformSettings->SupportsVulkanSM5(), TEXT("SupportsES31"), AndroidTargetPlatformSettings->SupportsES31() ); AppendAnalyticsEventConfigBool(AnalyticsParamArray, TEXT("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings"), TEXT("bPackageForMetaQuest"), GEngineIni); } #if WITH_ENGINE FName FAndroidTargetPlatformControls::FinalizeVirtualTextureLayerFormat(FName Format) const { #if WITH_EDITOR // Remap non-ETC variants to ETC // VirtualTexture Format was already run through the ordinary texture remaps to change AutoDXT to ASTC or ETC // this then runs again // currently it forces all ASTC to ETC // this is needed because the runtime virtual texture encoder only supports ETC // code dupe with IOSTargetPlatform // @todo Oodle: restrict this so it's only done when needed for RVT, not for all VT that would be better left as ASTC ; UE-212640 const static FName VTRemap[][2] = { { { FName(TEXT("ASTC_RGB")) }, { AndroidTexFormat::NameETC2_RGB } }, { { FName(TEXT("ASTC_RGBA")) }, { AndroidTexFormat::NameETC2_RGBA } }, { { FName(TEXT("ASTC_RGBA_HQ")) }, { AndroidTexFormat::NameETC2_RGBA } }, // { { FName(TEXT("ASTC_RGB_HDR")) }, { NameRGBA16F } }, // ? { { FName(TEXT("ASTC_RGBAuto")) }, { AndroidTexFormat::NameAutoETC2 } }, { { FName(TEXT("ASTC_NormalAG")) }, { AndroidTexFormat::NameETC2_RGB } }, { { AndroidTexFormat::NameASTC_NormalRG }, { AndroidTexFormat::NameETC2_RG11 } }, { { AndroidTexFormat::NameASTC_NormalLA }, { AndroidTexFormat::NameETC2_RG11 } }, { { AndroidTexFormat::NameDXT1 }, { AndroidTexFormat::NameETC2_RGB } }, { { AndroidTexFormat::NameDXT5 }, { AndroidTexFormat::NameETC2_RGBA } }, { { AndroidTexFormat::NameAutoDXT }, { AndroidTexFormat::NameAutoETC2 } } }; for (int32 RemapIndex = 0; RemapIndex < UE_ARRAY_COUNT(VTRemap); RemapIndex++) { if (VTRemap[RemapIndex][0] == Format) { return VTRemap[RemapIndex][1]; } } #endif return Format; } #endif //WITH_ENGINE bool FAndroidTargetPlatformControls::SupportsVariants() const { return true; } /* FAndroidTargetPlatformControls implementation *****************************************************************************/ void FAndroidTargetPlatformControls::InitializeDeviceDetection() { DeviceDetection = FModuleManager::LoadModuleChecked("AndroidDeviceDetection").GetAndroidDeviceDetection(); DeviceDetection->Initialize(TEXT("ANDROID_HOME"), #if PLATFORM_WINDOWS TEXT("platform-tools\\adb.exe"), #else TEXT("platform-tools/adb"), #endif TEXT("shell getprop"), true); } bool FAndroidTargetPlatformControls::ShouldExpandTo32Bit(const uint16* Indices, const int32 NumIndices) const { bool bIsMaliBugIndex = false; const uint16 MaliBugIndexMaxDiff = 16; uint16 LastIndex = Indices[0]; for (int32 i = 1; i < NumIndices; ++i) { uint16 CurrentIndex = Indices[i]; if ((FMath::Abs(LastIndex - CurrentIndex) > MaliBugIndexMaxDiff)) { bIsMaliBugIndex = true; break; } else { LastIndex = CurrentIndex; } } return bIsMaliBugIndex; } /* FAndroidTargetPlatformControls callbacks *****************************************************************************/ bool FAndroidTargetPlatformControls::HandleTicker( float DeltaTime ) { QUICK_SCOPE_CYCLE_COUNTER(STAT_FAndroidTargetPlatform_HandleTicker); if (DeviceDetection == nullptr) { InitializeDeviceDetection(); checkf(DeviceDetection != nullptr, TEXT("A target platform didn't create a device detection object in InitializeDeviceDetection()!")); } TArray ConnectedDeviceIds; { FScopeLock ScopeLock(DeviceDetection->GetDeviceMapLock()); for (const auto& Pair : DeviceDetection->GetDeviceMap()) { const FAndroidDeviceInfo& DeviceInfo = Pair.Value; // see if this device is already known if (FAndroidTargetDevicePtr TestDevice = Devices.FindRef(Pair.Key)) { // ignore if authorization didn't change if (DeviceInfo.bAuthorizedDevice == TestDevice->IsAuthorized() && DeviceInfo.SerialNumber == TestDevice->GetSerialNumber()) { ConnectedDeviceIds.Add(TestDevice->GetDeviceId()); continue; } // remove it to add again TestDevice->SetConnected(false); Devices.Remove(Pair.Key); OnDeviceLost().Broadcast(TestDevice.ToSharedRef()); } // check if this platform is supported by the extensions and version if (!SupportedByExtensionsString(DeviceInfo.GLESExtensions, DeviceInfo.GLESVersion)) { continue; } // create target device FAndroidTargetDevicePtr& Device = Devices.Add(Pair.Key); Device = CreateTargetDevice(*this, DeviceInfo.DeviceId, GetAndroidVariantName()); // we need a unique name for all devices, so use human usable display name and the unique id if (!DeviceInfo.SerialNumber.IsEmpty()) { if (!DeviceInfo.Model.IsEmpty()) { Device->SetName((!DeviceInfo.AvdName.IsEmpty() ? DeviceInfo.AvdName : DeviceInfo.Model) + TEXT(" (") + DeviceInfo.SerialNumber + TEXT(")")); } else { Device->SetName(DeviceInfo.SerialNumber + TEXT(" [UNAUTHORIZED]")); } Device->SetConnected(true); } else { Device->SetName(DeviceInfo.AvdName + TEXT(" [OFFLINE]")); Device->SetConnected(false); } Device->SetModel(DeviceInfo.Model); Device->SetDeviceName(DeviceInfo.DeviceName); Device->SetAuthorized(DeviceInfo.bAuthorizedDevice); Device->SetVersions(DeviceInfo.SDKVersion, DeviceInfo.HumanAndroidVersion); Device->SetArchitecture(DeviceInfo.Architecture); Device->SetSerialNumber(DeviceInfo.SerialNumber); ITargetPlatformControls::OnDeviceDiscovered().Broadcast(Device.ToSharedRef()); ConnectedDeviceIds.Add(Device->GetDeviceId()); } } // remove disconnected devices for (auto Iter = Devices.CreateIterator(); Iter; ++Iter) { if (!ConnectedDeviceIds.Contains(Iter.Key())) { FAndroidTargetDevicePtr RemovedDevice = Iter.Value(); RemovedDevice->SetConnected(false); Iter.RemoveCurrent(); OnDeviceLost().Broadcast(RemovedDevice.ToSharedRef()); } } return true; } FAndroidTargetDeviceRef FAndroidTargetPlatformControls::CreateNewDevice(const FAndroidDeviceInfo &DeviceInfo) { return MakeShareable(new FAndroidTargetDevice(*this, DeviceInfo.DeviceId, GetAndroidVariantName())); } FAndroidTargetDevicePtr FAndroidTargetPlatformControls::CreateTargetDevice(const ITargetPlatformControls& InTargetPlatform, const FString& InDeviceId, const FString& InAndroidVariant) const { return MakeShareable(new FAndroidTargetDevice(InTargetPlatform, InDeviceId, InAndroidVariant)); } #if WITH_ENGINE void FAndroidTargetPlatformControls::GetTextureFormats(const UTexture* Texture, TArray< TArray >& OutFormats) const { // FAndroidTargetPlatformControls aside from being the base class for all the concrete android target platforms // it is also usable on its own as "flavorless" Android // but I don't understand how that's supposed to work or what that's supposed to mean // and no information has been forthcoming check(Texture); // Supported in ES3.2 with ASTC const bool bSupportCompressedVolumeTexture = AndroidTargetPlatformSettings->SupportsTextureFormatCategory(EAndroidTextureFormatCategory::ASTC); // FWIW bSupportCompressedVolumeTexture should be true for Android_DXT but this is setting it to false // OpenGL ES has F32 textures but doesn't allow linear filtering unless OES_texture_float_linear const bool bSupportFilteredFloat32Textures = false; // optionaly compress landscape weightmaps for a mobile rendering bool bCompressLandscapeWeightMaps = false; GetTargetPlatformSettings()->GetConfigSystem()->GetBool(TEXT("/Script/Engine.RendererSettings"), TEXT("r.Mobile.CompressLandscapeWeightMaps"), bCompressLandscapeWeightMaps, GEngineIni); TArray& LayerFormats = OutFormats.AddDefaulted_GetRef(); int32 BlockSize = 1; // this looks wrong? should be 4 for FAndroid_DXTTargetPlatform ? - it is wrong, but BlockSize is ignored GetDefaultTextureFormatNamePerLayer(LayerFormats, this->GetTargetPlatformSettings(), this, Texture, bSupportCompressedVolumeTexture, BlockSize, bSupportFilteredFloat32Textures); for (FName& TextureFormatName : LayerFormats) { // @todo Oodle: this should not be here // should be in GetDefaultTextureFormatNamePerLayer // so that 4x4 checks can be applied correctly, etc. if (Texture->LODGroup == TEXTUREGROUP_Terrain_Weightmap && bCompressLandscapeWeightMaps) { TextureFormatName = AndroidTexFormat::NameAutoDXT; } if (Texture->GetTextureClass() == ETextureClass::Cube) { FTextureFormatSettings FormatSettings; Texture->GetDefaultFormatSettings(FormatSettings); // TC_EncodedReflectionCapture is no longer used and could be deleted if (FormatSettings.CompressionSettings == TC_EncodedReflectionCapture && !FormatSettings.CompressionNone) { TextureFormatName = FName(TEXT("ETC2_RGBA")); } } for (int32 RemapIndex = 0; RemapIndex < UE_ARRAY_COUNT(AndroidTexFormat::GenericRemap); ++RemapIndex) { if (TextureFormatName == AndroidTexFormat::GenericRemap[RemapIndex][0]) { TextureFormatName = AndroidTexFormat::GenericRemap[RemapIndex][1]; } } } } void FAndroidTargetPlatformControls::GetAllTextureFormats(TArray& OutFormats) const { GetAllDefaultTextureFormats(this->GetTargetPlatformSettings(), OutFormats); for (int32 RemapIndex = 0; RemapIndex < UE_ARRAY_COUNT(AndroidTexFormat::GenericRemap); ++RemapIndex) { OutFormats.Remove(AndroidTexFormat::GenericRemap[RemapIndex][0]); } for (int32 RemapIndex = 0; RemapIndex < UE_ARRAY_COUNT(AndroidTexFormat::GenericRemap); ++RemapIndex) { OutFormats.AddUnique(AndroidTexFormat::GenericRemap[RemapIndex][1]); } } void FAndroid_ASTCTargetPlatformControls::GetAllTextureFormats(TArray& OutFormats) const { FAndroidTargetPlatformControls::GetAllTextureFormats(OutFormats); for (int32 RemapIndex = 0; RemapIndex < UE_ARRAY_COUNT(AndroidTexFormat::ASTCRemap); ++RemapIndex) { OutFormats.Remove(AndroidTexFormat::ASTCRemap[RemapIndex][0]); } // ASTC for compressed textures OutFormats.Add(AndroidTexFormat::NameAutoASTC); // ETC for ETC2_R11 OutFormats.Add(AndroidTexFormat::NameAutoETC2); } void FAndroid_ASTCTargetPlatformControls::GetTextureFormats(const UTexture* Texture, TArray< TArray >& OutFormats) const { FAndroidTargetPlatformControls::GetTextureFormats(Texture, OutFormats); // L+A mode for normal map compression const bool bSupportsNormalLA = GetTargetPlatformSettings()->SupportsFeature(ETargetPlatformFeatures::NormalmapLAEncodingMode); // perform any remapping away from defaults TArray& LayerFormats = OutFormats.Last(); for (FName& TextureFormatName : LayerFormats) { if (bSupportsNormalLA && TextureFormatName == AndroidTexFormat::NameBC5) { TextureFormatName = AndroidTexFormat::NameASTC_NormalLA; continue; } for (int32 RemapIndex = 0; RemapIndex < UE_ARRAY_COUNT(AndroidTexFormat::ASTCRemap); ++RemapIndex) { if (TextureFormatName == AndroidTexFormat::ASTCRemap[RemapIndex][0]) { TextureFormatName = AndroidTexFormat::ASTCRemap[RemapIndex][1]; break; } } } bool bSupportASTCHDR = AndroidTargetPlatformSettings->UsesASTCHDR(); if (!bSupportASTCHDR) { for (FName& TextureFormatName : LayerFormats) { if (TextureFormatName == AndroidTexFormat::NameASTC_RGB_HDR) { TextureFormatName = GetTargetPlatformSettings()->GetFallbackASTCHDR(); } } } } void FAndroid_DXTTargetPlatformControls::GetTextureFormats(const UTexture* Texture, TArray< TArray >& OutFormats) const { FAndroidTargetPlatformControls::GetTextureFormats(Texture, OutFormats); bool bSupportsDX11Formats = false; // assume Android DXT does not support BC6/7 if (!bSupportsDX11Formats) { TArray& LayerFormats = OutFormats.Last(); for (FName& TextureFormatName : LayerFormats) { if (TextureFormatName == AndroidTexFormat::NameBC6H) { TextureFormatName = AndroidTexFormat::NameRGBA16F; } else if (TextureFormatName == AndroidTexFormat::NameBC7) { TextureFormatName = AndroidTexFormat::NameDXT5; } } } } void FAndroid_ETC2TargetPlatformControls::GetAllTextureFormats(TArray& OutFormats) const { FAndroidTargetPlatformControls::GetAllTextureFormats(OutFormats); for (int32 RemapIndex = 0; RemapIndex < UE_ARRAY_COUNT(AndroidTexFormat::ETCRemap); ++RemapIndex) { OutFormats.Remove(AndroidTexFormat::ETCRemap[RemapIndex][0]); } // support only ETC for compressed textures OutFormats.Add(AndroidTexFormat::NameAutoETC2); } void FAndroid_ETC2TargetPlatformControls::GetTextureFormats(const UTexture* Texture, TArray< TArray >& OutFormats) const { FAndroidTargetPlatformControls::GetTextureFormats(Texture, OutFormats); // perform any remapping away from defaults TArray& LayerFormats = OutFormats.Last(); for (FName& TextureFormatName : LayerFormats) { for (int32 RemapIndex = 0; RemapIndex < UE_ARRAY_COUNT(AndroidTexFormat::ETCRemap); ++RemapIndex) { if (TextureFormatName == AndroidTexFormat::ETCRemap[RemapIndex][0]) { TextureFormatName = AndroidTexFormat::ETCRemap[RemapIndex][1]; break; } } } } void FAndroid_MultiTargetPlatformControls::GetTextureFormats(const UTexture* Texture, TArray< TArray >& OutFormats) const { // Ask each platform variant to choose texture formats for (ITargetPlatformControls* Platform : FormatTargetPlatforms) { TArray< TArray > PlatformFormats; Platform->GetTextureFormats(Texture, PlatformFormats); for (TArray& FormatPerLayer : PlatformFormats) { // For multiformat case we have to disable L+A normal map compression as only ASTC textures support it for (FName& TextureFormatName : FormatPerLayer) { if (TextureFormatName == AndroidTexFormat::NameASTC_NormalLA) { TextureFormatName = AndroidTexFormat::NameASTC_NormalRG; } } OutFormats.AddUnique(FormatPerLayer); } } } void FAndroid_MultiTargetPlatformControls::GetAllTextureFormats(TArray& OutFormats) const { // Ask each platform variant to choose texture formats for (ITargetPlatformControls* Platform : FormatTargetPlatforms) { TArray PlatformFormats; Platform->GetAllTextureFormats(PlatformFormats); for (FName Format : PlatformFormats) { OutFormats.AddUnique(Format); } } } #endif FText FAndroid_MultiTargetPlatformControls::DisplayName() const { return FText::Format(LOCTEXT("Android_Multi", "Android (Multi:{0})"), FText::FromString(FormatTargetString)); } #undef LOCTEXT_NAMESPACE