// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. #include "AndroidPlatformEditorPrivatePCH.h" #include "AndroidTargetSettingsCustomization.h" #include "DetailLayoutBuilder.h" #include "DetailCategoryBuilder.h" #include "PropertyEditing.h" #include "ScopedTransaction.h" #include "SExternalImageReference.h" #include "SHyperlinkLaunchURL.h" #include "SPlatformSetupMessage.h" #include "PlatformIconInfo.h" #include "SourceControlHelpers.h" #include "ManifestUpdateHelper.h" #include "SNotificationList.h" #include "NotificationManager.h" #define LOCTEXT_NAMESPACE "AndroidRuntimeSettings" ////////////////////////////////////////////////////////////////////////// // FAndroidTargetSettingsCustomization namespace FAndroidTargetSettingsCustomizationConstants { const FText DisabledTip = LOCTEXT("GitHubSourceRequiredToolTip", "This requires GitHub source."); } TSharedRef FAndroidTargetSettingsCustomization::MakeInstance() { return MakeShareable(new FAndroidTargetSettingsCustomization); } FAndroidTargetSettingsCustomization::FAndroidTargetSettingsCustomization() : AndroidRelativePath(TEXT("")) , EngineAndroidPath(FPaths::EngineDir() + TEXT("Build/Android/Java")) , GameAndroidPath(FPaths::GameDir() + TEXT("Build/Android")) , EngineGooglePlayAppIDPath(EngineAndroidPath / TEXT("res") / TEXT("values") / TEXT("GooglePlayAppID.xml")) , GameGooglePlayAppIDPath(GameAndroidPath / TEXT("res") / TEXT("values") / TEXT("GooglePlayAppID.xml")) , EngineProguardPath(EngineAndroidPath / TEXT("proguard-project.txt")) , GameProguardPath(GameAndroidPath / TEXT("proguard-project.txt")) , EngineProjectPropertiesPath(EngineAndroidPath / TEXT("project.properties")) , GameProjectPropertiesPath(GameAndroidPath / TEXT("project.properties")) { new (IconNames) FPlatformIconInfo(TEXT("res/drawable/icon.png"), LOCTEXT("SettingsIcon", "Icon"), FText::GetEmpty(), 48, 48, FPlatformIconInfo::Required); new (IconNames) FPlatformIconInfo(TEXT("res/drawable-ldpi/icon.png"), LOCTEXT("SettingsIcon_LDPI", "LDPI Icon"), FText::GetEmpty(), 36, 36, FPlatformIconInfo::Required); new (IconNames) FPlatformIconInfo(TEXT("res/drawable-mdpi/icon.png"), LOCTEXT("SettingsIcon_MDPI", "MDPI Icon"), FText::GetEmpty(), 48, 48, FPlatformIconInfo::Required); new (IconNames) FPlatformIconInfo(TEXT("res/drawable-hdpi/icon.png"), LOCTEXT("SettingsIcon_HDPI", "HDPI Icon"), FText::GetEmpty(), 72, 72, FPlatformIconInfo::Required); new (IconNames) FPlatformIconInfo(TEXT("res/drawable-xhdpi/icon.png"), LOCTEXT("SettingsIcon_XHDPI", "XHDPI Icon"), FText::GetEmpty(), 96, 96, FPlatformIconInfo::Required); } void FAndroidTargetSettingsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) { SavedLayoutBuilder = &DetailLayout; BuildAppManifestSection(DetailLayout); BuildIconSection(DetailLayout); } static void OnBrowserLinkClicked(const FSlateHyperlinkRun::FMetadata& Metadata) { const FString* URL = Metadata.Find(TEXT("href")); if(URL) { FPlatformProcess::LaunchURL(**URL, nullptr, nullptr); } } void FAndroidTargetSettingsCustomization::BuildAppManifestSection(IDetailLayoutBuilder& DetailLayout) { // Cache some categories IDetailCategoryBuilder& APKPackagingCategory = DetailLayout.EditCategory(TEXT("APKPackaging")); IDetailCategoryBuilder& BuildCategory = DetailLayout.EditCategory(TEXT("Build")); IDetailCategoryBuilder& SigningCategory = DetailLayout.EditCategory(TEXT("DistributionSigning")); TSharedRef PlatformSetupMessage = SNew(SPlatformSetupMessage, GameProjectPropertiesPath) .PlatformName(LOCTEXT("AndroidPlatformName", "Android")) .OnSetupClicked(this, &FAndroidTargetSettingsCustomization::CopySetupFilesIntoProject); SetupForPlatformAttribute = PlatformSetupMessage->GetReadyToGoAttribute(); APKPackagingCategory.AddCustomRow(LOCTEXT("Warning", "Warning"), false) .WholeRowWidget [ PlatformSetupMessage ]; APKPackagingCategory.AddCustomRow(LOCTEXT("UpgradeInfo", "Upgrade Info"), false) .WholeRowWidget [ SNew(SBorder) .Padding(1) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(10, 10, 10, 10)) .FillWidth(1.0f) [ SNew(SRichTextBlock) .Text(LOCTEXT("UpgradeInfoMessage", "Note to users from 4.6 or earlier: We now GENERATE an AndroidManifest.xml when building, so if you have customized your .xml file, you will need to put all of your changes into the below settings. Note that we don't touch your AndroidManifest.xml that is in your project directory.\nAdditionally, we no longer use SigningConfig.xml, the settings are now set in the Distribution Signing section.\n\nThere is currently no .obb file downloader support in the engine, so if you don't package your data into your .apk (see the below setting and its tooltip about 50MB limit), device is not guaranteed to have the .obb file downloaded in all cases. Until Unreal Engine v4.8, there won't be a way for your app to download the .obb file from the Google Play Store. See http://developer.android.com/google/play/expansion-files.html for more information.")) .TextStyle(FEditorStyle::Get(), "MessageLog") .DecoratorStyleSet(&FEditorStyle::Get()) .AutoWrapText(true) + SRichTextBlock::HyperlinkDecorator(TEXT("browser"), FSlateHyperlinkRun::FOnClick::CreateStatic(&OnBrowserLinkClicked)) ] ] ]; APKPackagingCategory.AddCustomRow(LOCTEXT("BuildFolderLabel", "Build Folder"), false) .IsEnabled(SetupForPlatformAttribute) .NameContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(0, 1, 0, 1)) .FillWidth(1.0f) [ SNew(STextBlock) .Text(LOCTEXT("BuildFolderLabel", "Build Folder")) .Font(DetailLayout.GetDetailFont()) ] ] .ValueContent() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SButton) .Text(LOCTEXT("OpenBuildFolderButton", "Open Build Folder")) .ToolTipText(LOCTEXT("OpenManifestFolderButton_Tooltip", "Opens the folder containing the build files in Explorer or Finder (it's recommended you check these in to source control to share with your team)")) .OnClicked(this, &FAndroidTargetSettingsCustomization::OpenBuildFolder) ] ]; // Signing category SigningCategory.AddCustomRow(LOCTEXT("SigningHyperlink", "Signing Hyperlink"), false) .WholeRowWidget [ SNew(SBox) .HAlign(HAlign_Center) [ SNew(SHyperlinkLaunchURL, TEXT("http://developer.android.com/tools/publishing/app-signing.html#releasemode")) .Text(LOCTEXT("AndroidDeveloperSigningPage", "Android Developer page on Signing for Distribution")) .ToolTipText(LOCTEXT("AndroidDeveloperSigningPageTooltip", "Opens a page that discusses the signing using keytool")) ] ]; // Google Play category IDetailCategoryBuilder& GooglePlayCategory = DetailLayout.EditCategory(TEXT("GooglePlayServices")); TSharedRef GooglePlaySetupMessage = SNew(SPlatformSetupMessage, GameGooglePlayAppIDPath) .PlatformName(LOCTEXT("GooglePlayPlatformName", "Google Play services")) .OnSetupClicked(this, &FAndroidTargetSettingsCustomization::CopyGooglePlayAppIDFileIntoProject); SetupForGooglePlayAttribute = GooglePlaySetupMessage->GetReadyToGoAttribute(); GooglePlayCategory.AddCustomRow(LOCTEXT("Warning", "Warning"), false) .WholeRowWidget [ GooglePlaySetupMessage ]; GooglePlayCategory.AddCustomRow(LOCTEXT("AppIDHyperlink", "App ID Hyperlink"), false) .WholeRowWidget [ SNew(SBox) .HAlign(HAlign_Center) [ SNew(SHyperlinkLaunchURL, TEXT("http://developer.android.com/google/index.html")) .Text(LOCTEXT("GooglePlayDeveloperPage", "Android Developer Page on Google Play services")) .ToolTipText(LOCTEXT("GooglePlayDeveloperPageTooltip", "Opens a page that discusses Google Play services")) ] ]; TSharedRef EnabledProperty = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UAndroidRuntimeSettings, bEnableGooglePlaySupport)); GooglePlayCategory.AddProperty(EnabledProperty) .EditCondition(SetupForGooglePlayAttribute, NULL); TSharedRef AppIDProperty = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UAndroidRuntimeSettings, GamesAppID)); AppIDProperty->SetOnPropertyValueChanged(FSimpleDelegate::CreateRaw(this, &FAndroidTargetSettingsCustomization::OnAppIDModified)); GooglePlayCategory.AddProperty(AppIDProperty) .EditCondition(SetupForGooglePlayAttribute, NULL); TSharedRef AdMobAdUnitIDProperty = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UAndroidRuntimeSettings, AdMobAdUnitID)); GooglePlayCategory.AddProperty(AdMobAdUnitIDProperty) .EditCondition(SetupForGooglePlayAttribute, NULL); TSharedRef GooglePlayLicenseKeyProperty = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UAndroidRuntimeSettings, GooglePlayLicenseKey)); GooglePlayCategory.AddProperty(GooglePlayLicenseKeyProperty) .EditCondition(SetupForGooglePlayAttribute, NULL); #define SETUP_NONROCKET_PROP(PropName, Category, Tip) \ { \ TSharedRef PropertyHandle = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UAndroidRuntimeSettings, PropName)); \ Category.AddProperty(PropertyHandle) \ .EditCondition(SetupForPlatformAttribute, NULL) \ .IsEnabled(!FRocketSupport::IsRocket()) \ .ToolTip(!FRocketSupport::IsRocket() ? Tip : FAndroidTargetSettingsCustomizationConstants::DisabledTip); \ } SETUP_NONROCKET_PROP(bBuildForArmV7, BuildCategory, LOCTEXT("BuildForArmV7ToolTip", "Enable ArmV7 CPU architecture support? (this will be used if all CPU architecture types are unchecked)")); SETUP_NONROCKET_PROP(bBuildForX86, BuildCategory, LOCTEXT("BuildForX86ToolTip", "Enable X86 CPU architecture support?")); SETUP_NONROCKET_PROP(bBuildForES2, BuildCategory, LOCTEXT("BuildForES2ToolTip", "Enable OpenGL ES2 rendering support? (this will be used if rendering types are unchecked)")); SETUP_NONROCKET_PROP(bBuildForES31, BuildCategory, LOCTEXT("BuildForES31ToolTip", "Enable OpenGL ES31 + AEP (Android Extension Pack) rendering support? Currently only Tegra K1 supports this, as it will force DXT textures (In 4.8 3.1+AEP will work with all texture formats).\nIf you use the Launch On feature (in the main toolbar), when you change this setting, you need to restart the editor to make sure it will launch with the proper 3.1+AEP support!")); // @todo android fat binary: Put back in when we expose those // SETUP_NONROCKET_PROP(bSplitIntoSeparateApks, BuildCategory, LOCTEXT("SplitIntoSeparateAPKsToolTip", "If checked, CPU architectures and rendering types will be split into separate .apk files")); } void FAndroidTargetSettingsCustomization::BuildIconSection(IDetailLayoutBuilder& DetailLayout) { // Icon category IDetailCategoryBuilder& IconCategory = DetailLayout.EditCategory(TEXT("Icons")); IconCategory.AddCustomRow(LOCTEXT("IconsHyperlink", "Icons Hyperlink"), false) .WholeRowWidget [ SNew(SBox) .HAlign(HAlign_Center) [ SNew(SHyperlinkLaunchURL, TEXT("http://developer.android.com/design/style/iconography.html")) .Text(LOCTEXT("AndroidDeveloperIconographyPage", "Android Developer Page on Iconography")) .ToolTipText(LOCTEXT("AndroidDeveloperIconographyPageTooltip", "Opens a page on Android Iconography")) ] ]; for (const FPlatformIconInfo& Info : IconNames) { const FString AutomaticImagePath = EngineAndroidPath / Info.IconPath; const FString TargetImagePath = GameAndroidPath / Info.IconPath; IconCategory.AddCustomRow(Info.IconName) .NameContent() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .Padding( FMargin( 0, 1, 0, 1 ) ) .FillWidth(1.0f) [ SNew(STextBlock) .Text(Info.IconName) .Font(DetailLayout.GetDetailFont()) ] ] .ValueContent() .MaxDesiredWidth(400.0f) .MinDesiredWidth(100.0f) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) [ SNew(SExternalImageReference, AutomaticImagePath, TargetImagePath) .FileDescription(Info.IconDescription) .RequiredSize(Info.IconRequiredSize) .MaxDisplaySize(FVector2D(Info.IconRequiredSize)) ] ]; } } FReply FAndroidTargetSettingsCustomization::OpenBuildFolder() { const FString BuildFolder = FPaths::ConvertRelativePathToFull(FPaths::GetPath(GameProjectPropertiesPath)); FPlatformProcess::ExploreFolder(*BuildFolder); return FReply::Handled(); } void FAndroidTargetSettingsCustomization::CopySetupFilesIntoProject() { // First copy the manifest, it must get copied FText ErrorMessage; if (!SourceControlHelpers::CopyFileUnderSourceControl(GameProjectPropertiesPath, EngineProjectPropertiesPath, LOCTEXT("ProjectProperties", "Project Properties"), /*out*/ ErrorMessage)) { FNotificationInfo Info(ErrorMessage); Info.ExpireDuration = 3.0f; FSlateNotificationManager::Get().AddNotification(Info); } else { // Now try to copy all of the icons, etc... (these can be ignored if the file already exists) for (const FPlatformIconInfo& Info : IconNames) { const FString EngineImagePath = EngineAndroidPath / Info.IconPath; const FString ProjectImagePath = GameAndroidPath / Info.IconPath; if (!FPaths::FileExists(ProjectImagePath)) { SourceControlHelpers::CopyFileUnderSourceControl(ProjectImagePath, EngineImagePath, Info.IconName, /*out*/ ErrorMessage); } } // and copy the other files (aren't required) SourceControlHelpers::CopyFileUnderSourceControl(GameProguardPath, EngineProguardPath, LOCTEXT("Proguard", "Proguard Settings"), /*out*/ ErrorMessage); } SavedLayoutBuilder->ForceRefreshDetails(); } void FAndroidTargetSettingsCustomization::CopyGooglePlayAppIDFileIntoProject() { FText ErrorMessage; if (!SourceControlHelpers::CopyFileUnderSourceControl(GameGooglePlayAppIDPath, EngineGooglePlayAppIDPath, LOCTEXT("GooglePlayAppID", "GooglePlayAppID.xml"), /*out*/ ErrorMessage)) { FNotificationInfo Info(ErrorMessage); Info.ExpireDuration = 3.0f; FSlateNotificationManager::Get().AddNotification(Info); } SavedLayoutBuilder->ForceRefreshDetails(); } void FAndroidTargetSettingsCustomization::OnAppIDModified() { check(SetupForPlatformAttribute.Get() == true); FManifestUpdateHelper Updater(GameGooglePlayAppIDPath); const FString AppIDTag(TEXT("name=\"app_id\">")); const FString ClosingTag(TEXT("")); const FString NewIDString = GetDefault()->GamesAppID; Updater.ReplaceKey(AppIDTag, ClosingTag, NewIDString); Updater.Finalize(GameGooglePlayAppIDPath); } ////////////////////////////////////////////////////////////////////////// #undef LOCTEXT_NAMESPACE