1 /* 2 ============================================================================== 3 4 This file is part of the JUCE library. 5 Copyright (c) 2020 - Raw Material Software Limited 6 7 JUCE is an open source library subject to commercial or open-source 8 licensing. 9 10 By using JUCE, you agree to the terms of both the JUCE 6 End-User License 11 Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). 12 13 End User License Agreement: www.juce.com/juce-6-licence 14 Privacy Policy: www.juce.com/juce-privacy-policy 15 16 Or: You may also use this code under the terms of the GPL v3 (see 17 www.gnu.org/licenses). 18 19 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER 20 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE 21 DISCLAIMED. 22 23 ============================================================================== 24 */ 25 26 #pragma once 27 28 29 //============================================================================== 30 class AndroidProjectExporter : public ProjectExporter 31 { 32 public: 33 //============================================================================== isXcode()34 bool isXcode() const override { return false; } isVisualStudio()35 bool isVisualStudio() const override { return false; } isCodeBlocks()36 bool isCodeBlocks() const override { return false; } isMakefile()37 bool isMakefile() const override { return false; } isAndroidStudio()38 bool isAndroidStudio() const override { return true; } isCLion()39 bool isCLion() const override { return false; } 40 isAndroid()41 bool isAndroid() const override { return true; } isWindows()42 bool isWindows() const override { return false; } isLinux()43 bool isLinux() const override { return false; } isOSX()44 bool isOSX() const override { return false; } isiOS()45 bool isiOS() const override { return false; } 46 usesMMFiles()47 bool usesMMFiles() const override { return false; } canCopeWithDuplicateFiles()48 bool canCopeWithDuplicateFiles() override { return false; } supportsUserDefinedConfigurations()49 bool supportsUserDefinedConfigurations() const override { return true; } 50 getNewLineString()51 String getNewLineString() const override { return "\n"; } 52 supportsTargetType(build_tools::ProjectType::Target::Type type)53 bool supportsTargetType (build_tools::ProjectType::Target::Type type) const override 54 { 55 return type == build_tools::ProjectType::Target::GUIApp || type == build_tools::ProjectType::Target::StaticLibrary 56 || type == build_tools::ProjectType::Target::DynamicLibrary || type == build_tools::ProjectType::Target::StandalonePlugIn; 57 } 58 59 //============================================================================== addPlatformSpecificSettingsForProjectType(const build_tools::ProjectType &)60 void addPlatformSpecificSettingsForProjectType (const build_tools::ProjectType&) override 61 { 62 // no-op. 63 } 64 65 //============================================================================== createExporterProperties(PropertyListBuilder & props)66 void createExporterProperties (PropertyListBuilder& props) override 67 { 68 createBaseExporterProperties (props); 69 createToolchainExporterProperties (props); 70 createManifestExporterProperties (props); 71 createCodeSigningExporterProperties (props); 72 createOtherExporterProperties (props); 73 } 74 getDisplayName()75 static String getDisplayName() { return "Android"; } getValueTreeTypeName()76 static String getValueTreeTypeName() { return "ANDROIDSTUDIO"; } getTargetFolderName()77 static String getTargetFolderName() { return "Android"; } 78 getDefaultActivityClass()79 static const char* getDefaultActivityClass() { return "com.rmsl.juce.JuceActivity"; } getDefaultApplicationClass()80 static const char* getDefaultApplicationClass() { return "com.rmsl.juce.JuceApp"; } 81 createForSettings(Project & projectToUse,const ValueTree & settingsToUse)82 static AndroidProjectExporter* createForSettings (Project& projectToUse, const ValueTree& settingsToUse) 83 { 84 if (settingsToUse.hasType (getValueTreeTypeName())) 85 return new AndroidProjectExporter (projectToUse, settingsToUse); 86 87 return {}; 88 } 89 90 //============================================================================== 91 ValueWithDefault androidJavaLibs, androidAdditionalJavaFolders, androidAdditionalResourceFolders, androidProjectRepositories, androidRepositories, androidDependencies, androidCustomAppBuildGradleContent, 92 androidScreenOrientation, androidCustomActivityClass, androidCustomApplicationClass, androidManifestCustomXmlElements, androidGradleSettingsContent, androidVersionCode, 93 androidMinimumSDK, androidTargetSDK, androidTheme, androidExtraAssetsFolder, androidOboeRepositoryPath, androidInternetNeeded, androidMicNeeded, androidCameraNeeded, 94 androidBluetoothNeeded, androidExternalReadPermission, androidExternalWritePermission, androidInAppBillingPermission, androidVibratePermission, androidOtherPermissions, 95 androidPushNotifications, androidEnableRemoteNotifications, androidRemoteNotificationsConfigFile, androidEnableContentSharing, androidKeyStore, androidKeyStorePass, 96 androidKeyAlias, androidKeyAliasPass, gradleVersion, gradleToolchain, androidPluginVersion; 97 98 //============================================================================== AndroidProjectExporter(Project & p,const ValueTree & t)99 AndroidProjectExporter (Project& p, const ValueTree& t) 100 : ProjectExporter (p, t), 101 androidJavaLibs (settings, Ids::androidJavaLibs, getUndoManager()), 102 androidAdditionalJavaFolders (settings, Ids::androidAdditionalJavaFolders, getUndoManager()), 103 androidAdditionalResourceFolders (settings, Ids::androidAdditionalResourceFolders, getUndoManager()), 104 androidProjectRepositories (settings, Ids::androidProjectRepositories, getUndoManager(), "google()\njcenter()"), 105 androidRepositories (settings, Ids::androidRepositories, getUndoManager()), 106 androidDependencies (settings, Ids::androidDependencies, getUndoManager()), 107 androidCustomAppBuildGradleContent (settings, Ids::androidCustomAppBuildGradleContent, getUndoManager()), 108 androidScreenOrientation (settings, Ids::androidScreenOrientation, getUndoManager(), "unspecified"), 109 androidCustomActivityClass (settings, Ids::androidCustomActivityClass, getUndoManager()), 110 androidCustomApplicationClass (settings, Ids::androidCustomApplicationClass, getUndoManager(), getDefaultApplicationClass()), 111 androidManifestCustomXmlElements (settings, Ids::androidManifestCustomXmlElements, getUndoManager()), 112 androidGradleSettingsContent (settings, Ids::androidGradleSettingsContent, getUndoManager()), 113 androidVersionCode (settings, Ids::androidVersionCode, getUndoManager(), "1"), 114 androidMinimumSDK (settings, Ids::androidMinimumSDK, getUndoManager(), "16"), 115 androidTargetSDK (settings, Ids::androidTargetSDK, getUndoManager(), "29"), 116 androidTheme (settings, Ids::androidTheme, getUndoManager()), 117 androidExtraAssetsFolder (settings, Ids::androidExtraAssetsFolder, getUndoManager()), 118 androidOboeRepositoryPath (settings, Ids::androidOboeRepositoryPath, getUndoManager()), 119 androidInternetNeeded (settings, Ids::androidInternetNeeded, getUndoManager(), true), 120 androidMicNeeded (settings, Ids::microphonePermissionNeeded, getUndoManager(), false), 121 androidCameraNeeded (settings, Ids::cameraPermissionNeeded, getUndoManager(), false), 122 androidBluetoothNeeded (settings, Ids::androidBluetoothNeeded, getUndoManager(), true), 123 androidExternalReadPermission (settings, Ids::androidExternalReadNeeded, getUndoManager(), true), 124 androidExternalWritePermission (settings, Ids::androidExternalWriteNeeded, getUndoManager(), true), 125 androidInAppBillingPermission (settings, Ids::androidInAppBilling, getUndoManager(), false), 126 androidVibratePermission (settings, Ids::androidVibratePermissionNeeded, getUndoManager(), false), 127 androidOtherPermissions (settings, Ids::androidOtherPermissions, getUndoManager()), 128 androidPushNotifications (settings, Ids::androidPushNotifications, getUndoManager(), ! isLibrary()), 129 androidEnableRemoteNotifications (settings, Ids::androidEnableRemoteNotifications, getUndoManager(), false), 130 androidRemoteNotificationsConfigFile (settings, Ids::androidRemoteNotificationsConfigFile, getUndoManager()), 131 androidEnableContentSharing (settings, Ids::androidEnableContentSharing, getUndoManager(), false), 132 androidKeyStore (settings, Ids::androidKeyStore, getUndoManager(), "${user.home}/.android/debug.keystore"), 133 androidKeyStorePass (settings, Ids::androidKeyStorePass, getUndoManager(), "android"), 134 androidKeyAlias (settings, Ids::androidKeyAlias, getUndoManager(), "androiddebugkey"), 135 androidKeyAliasPass (settings, Ids::androidKeyAliasPass, getUndoManager(), "android"), 136 gradleVersion (settings, Ids::gradleVersion, getUndoManager(), "6.1.1"), 137 gradleToolchain (settings, Ids::gradleToolchain, getUndoManager(), "clang"), 138 androidPluginVersion (settings, Ids::androidPluginVersion, getUndoManager(), "4.0.0"), 139 AndroidExecutable (getAppSettings().getStoredPath (Ids::androidStudioExePath, TargetOS::getThisOS()).get().toString()) 140 { 141 name = getDisplayName(); 142 targetLocationValue.setDefault (getDefaultBuildsRootFolder() + getTargetFolderName()); 143 } 144 145 //============================================================================== createToolchainExporterProperties(PropertyListBuilder & props)146 void createToolchainExporterProperties (PropertyListBuilder& props) 147 { 148 props.add (new TextPropertyComponent (gradleVersion, "Gradle Version", 32, false), 149 "The version of gradle that is used to build this app (4.10 is fine for JUCE)"); 150 151 props.add (new TextPropertyComponent (androidPluginVersion, "Android Plug-in Version", 32, false), 152 "The version of the android build plugin for gradle that is used to build this app"); 153 154 props.add (new ChoicePropertyComponent (gradleToolchain, "NDK Toolchain", 155 { "clang", "gcc" }, 156 { "clang", "gcc" }), 157 "The toolchain that gradle should invoke for NDK compilation (variable model.android.ndk.tooclhain in app/build.gradle)"); 158 } 159 160 //============================================================================== canLaunchProject()161 bool canLaunchProject() override 162 { 163 return AndroidExecutable.exists(); 164 } 165 launchProject()166 bool launchProject() override 167 { 168 if (! AndroidExecutable.exists()) 169 { 170 jassertfalse; 171 return false; 172 } 173 174 auto targetFolder = getTargetFolder(); 175 176 // we have to surround the path with extra quotes, otherwise Android Studio 177 // will choke if there are any space characters in the path. 178 return AndroidExecutable.startAsProcess ("\"" + targetFolder.getFullPathName() + "\""); 179 } 180 181 //============================================================================== create(const OwnedArray<LibraryModule> & modules)182 void create (const OwnedArray<LibraryModule>& modules) const override 183 { 184 auto targetFolder = getTargetFolder(); 185 auto appFolder = targetFolder.getChildFile (isLibrary() ? "lib" : "app"); 186 187 removeOldFiles (targetFolder); 188 copyExtraResourceFiles(); 189 190 writeFile (targetFolder, "settings.gradle", getGradleSettingsFileContent()); 191 writeFile (targetFolder, "build.gradle", getProjectBuildGradleFileContent()); 192 writeFile (appFolder, "build.gradle", getAppBuildGradleFileContent (modules)); 193 writeFile (targetFolder, "local.properties", getLocalPropertiesFileContent()); 194 writeFile (targetFolder, "gradle/wrapper/gradle-wrapper.properties", getGradleWrapperPropertiesFileContent()); 195 196 writeBinaryFile (targetFolder, "gradle/wrapper/LICENSE-for-gradlewrapper.txt", BinaryData::LICENSE, BinaryData::LICENSESize); 197 writeBinaryFile (targetFolder, "gradle/wrapper/gradle-wrapper.jar", BinaryData::gradlewrapper_jar, BinaryData::gradlewrapper_jarSize); 198 writeBinaryFile (targetFolder, "gradlew", BinaryData::gradlew, BinaryData::gradlewSize); 199 writeBinaryFile (targetFolder, "gradlew.bat", BinaryData::gradlew_bat, BinaryData::gradlew_batSize); 200 201 targetFolder.getChildFile ("gradlew").setExecutePermission (true); 202 203 writeAndroidManifest (appFolder); 204 205 if (! isLibrary()) 206 { 207 copyAdditionalJavaLibs (appFolder); 208 writeStringsXML (targetFolder); 209 writeAppIcons (targetFolder); 210 } 211 212 writeCmakeFile (appFolder.getChildFile ("CMakeLists.txt")); 213 214 auto androidExtraAssetsFolderValue = androidExtraAssetsFolder.get().toString(); 215 216 if (androidExtraAssetsFolderValue.isNotEmpty()) 217 { 218 auto extraAssets = getProject().getFile().getParentDirectory().getChildFile (androidExtraAssetsFolderValue); 219 220 if (extraAssets.exists() && extraAssets.isDirectory()) 221 { 222 auto assetsFolder = appFolder.getChildFile ("src/main/assets"); 223 224 if (assetsFolder.deleteRecursively()) 225 extraAssets.copyDirectoryTo (assetsFolder); 226 } 227 } 228 } 229 removeOldFiles(const File & targetFolder)230 void removeOldFiles (const File& targetFolder) const 231 { 232 targetFolder.getChildFile ("app/build").deleteRecursively(); 233 targetFolder.getChildFile ("app/build.gradle").deleteFile(); 234 targetFolder.getChildFile ("gradle").deleteRecursively(); 235 targetFolder.getChildFile ("local.properties").deleteFile(); 236 targetFolder.getChildFile ("settings.gradle").deleteFile(); 237 } 238 writeFile(const File & gradleProjectFolder,const String & filePath,const String & fileContent)239 void writeFile (const File& gradleProjectFolder, const String& filePath, const String& fileContent) const 240 { 241 build_tools::writeStreamToFile (gradleProjectFolder.getChildFile (filePath), [&] (MemoryOutputStream& mo) 242 { 243 mo.setNewLineString (getNewLineString()); 244 mo << fileContent; 245 }); 246 } 247 writeBinaryFile(const File & gradleProjectFolder,const String & filePath,const char * binaryData,const int binarySize)248 void writeBinaryFile (const File& gradleProjectFolder, const String& filePath, const char* binaryData, const int binarySize) const 249 { 250 build_tools::writeStreamToFile (gradleProjectFolder.getChildFile (filePath), [&] (MemoryOutputStream& mo) 251 { 252 mo.setNewLineString (getNewLineString()); 253 mo.write (binaryData, static_cast<size_t> (binarySize)); 254 }); 255 } 256 257 protected: 258 //============================================================================== 259 class AndroidBuildConfiguration : public BuildConfiguration 260 { 261 public: AndroidBuildConfiguration(Project & p,const ValueTree & settings,const ProjectExporter & e)262 AndroidBuildConfiguration (Project& p, const ValueTree& settings, const ProjectExporter& e) 263 : BuildConfiguration (p, settings, e), 264 androidArchitectures (config, Ids::androidArchitectures, getUndoManager(), isDebug() ? "armeabi-v7a x86 arm64-v8a x86_64" : ""), 265 androidBuildConfigRemoteNotifsConfigFile (config, Ids::androidBuildConfigRemoteNotifsConfigFile, getUndoManager()), 266 androidAdditionalXmlValueResources (config, Ids::androidAdditionalXmlValueResources, getUndoManager()), 267 androidAdditionalDrawableResources (config, Ids::androidAdditionalDrawableResources, getUndoManager()), 268 androidAdditionalRawValueResources (config, Ids::androidAdditionalRawValueResources, getUndoManager()), 269 androidCustomStringXmlElements (config, Ids::androidCustomStringXmlElements, getUndoManager()) 270 { 271 linkTimeOptimisationValue.setDefault (false); 272 optimisationLevelValue.setDefault (isDebug() ? gccO0 : gccO3); 273 } 274 getArchitectures()275 String getArchitectures() const { return androidArchitectures.get().toString(); } getRemoteNotifsConfigFile()276 String getRemoteNotifsConfigFile() const { return androidBuildConfigRemoteNotifsConfigFile.get().toString(); } getAdditionalXmlResources()277 String getAdditionalXmlResources() const { return androidAdditionalXmlValueResources.get().toString(); } getAdditionalDrawableResources()278 String getAdditionalDrawableResources() const { return androidAdditionalDrawableResources.get().toString(); } getAdditionalRawResources()279 String getAdditionalRawResources() const { return androidAdditionalRawValueResources.get().toString();} getCustomStringsXml()280 String getCustomStringsXml() const { return androidCustomStringXmlElements.get().toString(); } 281 createConfigProperties(PropertyListBuilder & props)282 void createConfigProperties (PropertyListBuilder& props) override 283 { 284 addRecommendedLLVMCompilerWarningsProperty (props); 285 addGCCOptimisationProperty (props); 286 287 props.add (new TextPropertyComponent (androidArchitectures, "Architectures", 256, false), 288 "A list of the architectures to build (for a fat binary). Leave empty to build for all possible android architectures."); 289 290 props.add (new TextPropertyComponent (androidBuildConfigRemoteNotifsConfigFile.getPropertyAsValue(), "Remote Notifications Config File", 2048, false), 291 "Path to google-services.json file. This will be the file provided by Firebase when creating a new app in Firebase console. " 292 "This will override the setting from the main Android exporter node."); 293 294 props.add (new TextPropertyComponent (androidAdditionalXmlValueResources, "Extra Android XML Value Resources", 8192, true), 295 "Paths to additional \"value resource\" files in XML format that should be included in the app (one per line). " 296 "If you have additional XML resources that should be treated as value resources, add them here."); 297 298 props.add (new TextPropertyComponent (androidAdditionalDrawableResources, "Extra Android Drawable Resources", 8192, true), 299 "Paths to additional \"drawable resource\" directories that should be included in the app (one per line). " 300 "They will be added to \"res\" directory of Android project. " 301 "Each path should point to a directory named \"drawable\" or \"drawable-<size>\" where <size> should be " 302 "something like \"hdpi\", \"ldpi\", \"xxxhdpi\" etc, for instance \"drawable-xhdpi\". " 303 "Refer to Android Studio documentation for available sizes."); 304 305 props.add (new TextPropertyComponent (androidAdditionalRawValueResources, "Extra Android Raw Resources", 8192, true), 306 "Paths to additional \"raw resource\" files that should be included in the app (one per line). " 307 "Resource file names must contain only lowercase a-z, 0-9 or underscore."); 308 309 props.add (new TextPropertyComponent (androidCustomStringXmlElements, "Custom String Resources", 8192, true), 310 "Custom XML resources that will be added to string.xml as children of <resources> element. " 311 "Example: \n<string name=\"value\">text</string>\n" 312 "<string name2=\"value2\">text2</string>\n"); 313 } 314 getProductFlavourNameIdentifier()315 String getProductFlavourNameIdentifier() const 316 { 317 return getName().toLowerCase().replaceCharacter (L' ', L'_') + String ("_"); 318 } 319 getProductFlavourCMakeIdentifier()320 String getProductFlavourCMakeIdentifier() const 321 { 322 return getName().toUpperCase().replaceCharacter (L' ', L'_'); 323 } 324 getModuleLibraryArchName()325 String getModuleLibraryArchName() const override 326 { 327 return "${ANDROID_ABI}"; 328 } 329 330 ValueWithDefault androidArchitectures, androidBuildConfigRemoteNotifsConfigFile, 331 androidAdditionalXmlValueResources, androidAdditionalDrawableResources, 332 androidAdditionalRawValueResources, androidCustomStringXmlElements; 333 }; 334 createBuildConfig(const ValueTree & v)335 BuildConfiguration::Ptr createBuildConfig (const ValueTree& v) const override 336 { 337 return *new AndroidBuildConfiguration (project, v, *this); 338 } 339 340 private: writeCmakeFile(const File & file)341 void writeCmakeFile (const File& file) const 342 { 343 build_tools::writeStreamToFile (file, [&] (MemoryOutputStream& mo) 344 { 345 mo.setNewLineString (getNewLineString()); 346 347 mo << "# Automatically generated makefile, created by the Projucer" << newLine 348 << "# Don't edit this file! Your changes will be overwritten when you re-save the Projucer project!" << newLine 349 << newLine; 350 351 mo << "cmake_minimum_required(VERSION 3.4.1)" << newLine << newLine; 352 353 if (! isLibrary()) 354 mo << "set(BINARY_NAME \"juce_jni\")" << newLine << newLine; 355 356 auto useOboe = project.getEnabledModules().isModuleEnabled ("juce_audio_devices") 357 && project.isConfigFlagEnabled ("JUCE_USE_ANDROID_OBOE", true); 358 359 if (useOboe) 360 { 361 auto oboePath = [&] 362 { 363 auto oboeDir = androidOboeRepositoryPath.get().toString().trim(); 364 365 if (oboeDir.isEmpty()) 366 oboeDir = getModuleFolderRelativeToProject ("juce_audio_devices").getChildFile ("native") 367 .getChildFile ("oboe") 368 .rebased (getProject().getProjectFolder(), getTargetFolder(), 369 build_tools::RelativePath::buildTargetFolder) 370 .toUnixStyle(); 371 372 // CMakeLists.txt is in the "app" subfolder 373 if (! build_tools::isAbsolutePath (oboeDir)) 374 oboeDir = "../" + oboeDir; 375 376 return expandHomeFolderToken (oboeDir); 377 }(); 378 379 mo << "set(OBOE_DIR " << oboePath.quoted() << ")" << newLine << newLine; 380 mo << "add_subdirectory (${OBOE_DIR} ./oboe)" << newLine << newLine; 381 } 382 383 String cpufeaturesPath ("${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c"); 384 mo << "add_library(\"cpufeatures\" STATIC \"" << cpufeaturesPath << "\")" << newLine 385 << "set_source_files_properties(\"" << cpufeaturesPath << "\" PROPERTIES COMPILE_FLAGS \"-Wno-sign-conversion -Wno-gnu-statement-expression\")" << newLine << newLine; 386 387 { 388 auto projectDefines = getEscapedPreprocessorDefs (getProjectPreprocessorDefs()); 389 if (projectDefines.size() > 0) 390 mo << "add_definitions(" << projectDefines.joinIntoString (" ") << ")" << newLine << newLine; 391 } 392 393 { 394 mo << "include_directories( AFTER" << newLine; 395 396 for (auto& path : extraSearchPaths) 397 mo << " \"" << escapeDirectoryForCmake (path) << "\"" << newLine; 398 399 mo << " \"${ANDROID_NDK}/sources/android/cpufeatures\"" << newLine; 400 401 mo << ")" << newLine << newLine; 402 } 403 404 auto cfgExtraLinkerFlags = getExtraLinkerFlagsString(); 405 if (cfgExtraLinkerFlags.isNotEmpty()) 406 { 407 mo << "set( JUCE_LDFLAGS \"" << cfgExtraLinkerFlags.replace ("\"", "\\\"") << "\")" << newLine; 408 mo << "set( CMAKE_SHARED_LINKER_FLAGS \"${CMAKE_EXE_LINKER_FLAGS} ${JUCE_LDFLAGS}\")" << newLine << newLine; 409 } 410 411 mo << "enable_language(ASM)" << newLine << newLine; 412 413 auto userLibraries = StringArray::fromTokens (getExternalLibrariesString(), ";", ""); 414 userLibraries.addArray (androidLibs); 415 416 if (getNumConfigurations() > 0) 417 { 418 bool first = true; 419 420 for (ConstConfigIterator config (*this); config.next();) 421 { 422 auto& cfg = dynamic_cast<const AndroidBuildConfiguration&> (*config); 423 auto libSearchPaths = cfg.getLibrarySearchPaths(); 424 auto cfgDefines = getConfigPreprocessorDefs (cfg); 425 auto cfgHeaderPaths = cfg.getHeaderSearchPaths(); 426 auto cfgLibraryPaths = cfg.getLibrarySearchPaths(); 427 428 if (! isLibrary() && libSearchPaths.size() == 0 && cfgDefines.size() == 0 429 && cfgHeaderPaths.size() == 0 && cfgLibraryPaths.size() == 0) 430 continue; 431 432 mo << (first ? "if" : "elseif") << "(JUCE_BUILD_CONFIGURATION MATCHES \"" << cfg.getProductFlavourCMakeIdentifier() <<"\")" << newLine; 433 434 if (isLibrary()) 435 { 436 mo << " set(BINARY_NAME \"" << getNativeModuleBinaryName (cfg) << "\")" << newLine; 437 438 auto binaryLocation = cfg.getTargetBinaryRelativePathString(); 439 440 if (binaryLocation.isNotEmpty()) 441 { 442 auto locationRelativeToCmake = build_tools::RelativePath (binaryLocation, build_tools::RelativePath::projectFolder) 443 .rebased (getProject().getFile().getParentDirectory(), 444 file.getParentDirectory(), build_tools::RelativePath::buildTargetFolder); 445 446 mo << " set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY \"" << "../../../../" << locationRelativeToCmake.toUnixStyle() << "\")" << newLine; 447 } 448 } 449 450 writeCmakePathLines (mo, " ", "link_directories(", libSearchPaths); 451 452 if (cfgDefines.size() > 0) 453 mo << " add_definitions(" << getEscapedPreprocessorDefs (cfgDefines).joinIntoString (" ") << ")" << newLine; 454 455 writeCmakePathLines (mo, " ", "include_directories( AFTER", cfgHeaderPaths); 456 457 if (userLibraries.size() > 0) 458 { 459 for (auto& lib : userLibraries) 460 { 461 String findLibraryCmd; 462 findLibraryCmd << "find_library(" << lib.toLowerCase().replaceCharacter (L' ', L'_') 463 << " \"" << lib << "\" PATHS"; 464 465 writeCmakePathLines (mo, " ", findLibraryCmd, cfgLibraryPaths, " NO_CMAKE_FIND_ROOT_PATH)"); 466 } 467 468 mo << newLine; 469 } 470 471 if (cfg.isLinkTimeOptimisationEnabled()) 472 { 473 // There's no MIPS support for LTO 474 String mipsCondition ("NOT (ANDROID_ABI STREQUAL \"mips\" OR ANDROID_ABI STREQUAL \"mips64\")"); 475 mo << " if(" << mipsCondition << ")" << newLine; 476 StringArray cmakeVariables ("CMAKE_C_FLAGS", "CMAKE_CXX_FLAGS", "CMAKE_EXE_LINKER_FLAGS"); 477 478 for (auto& variable : cmakeVariables) 479 { 480 auto configVariable = variable + "_" + cfg.getProductFlavourCMakeIdentifier(); 481 mo << " set(" << configVariable << " \"${" << configVariable << "} -flto\")" << newLine; 482 } 483 484 mo << " endif()" << newLine; 485 } 486 487 first = false; 488 } 489 490 if (! first) 491 { 492 ProjectExporter::BuildConfiguration::Ptr config (getConfiguration(0)); 493 494 if (config) 495 { 496 if (dynamic_cast<const AndroidBuildConfiguration*> (config.get()) != nullptr) 497 { 498 mo << "else()" << newLine; 499 mo << " message( FATAL_ERROR \"No matching build-configuration found.\" )" << newLine; 500 mo << "endif()" << newLine << newLine; 501 } 502 } 503 } 504 } 505 506 Array<build_tools::RelativePath> excludeFromBuild; 507 Array<std::pair<build_tools::RelativePath, String>> extraCompilerFlags; 508 509 mo << "add_library( ${BINARY_NAME}" << newLine; 510 mo << newLine; 511 mo << " " << (getProject().getProjectType().isStaticLibrary() ? "STATIC" : "SHARED") << newLine; 512 mo << newLine; 513 addCompileUnits (mo, excludeFromBuild, extraCompilerFlags); 514 mo << ")" << newLine << newLine; 515 516 if (excludeFromBuild.size() > 0) 517 { 518 for (auto& exclude : excludeFromBuild) 519 mo << "set_source_files_properties(\"" << exclude.toUnixStyle() << "\" PROPERTIES HEADER_FILE_ONLY TRUE)" << newLine; 520 521 mo << newLine; 522 } 523 524 if (! extraCompilerFlags.isEmpty()) 525 { 526 for (auto& extra : extraCompilerFlags) 527 mo << "set_source_files_properties(\"" << extra.first.toUnixStyle() << "\" PROPERTIES COMPILE_FLAGS " << extra.second << " )" << newLine; 528 529 mo << newLine; 530 } 531 532 auto flags = getProjectCompilerFlags(); 533 534 if (flags.size() > 0) 535 mo << "target_compile_options( ${BINARY_NAME} PRIVATE " << flags.joinIntoString (" ") << " )" << newLine << newLine; 536 537 for (ConstConfigIterator config (*this); config.next();) 538 { 539 auto& cfg = dynamic_cast<const AndroidBuildConfiguration&> (*config); 540 541 mo << "if( JUCE_BUILD_CONFIGURATION MATCHES \"" << cfg.getProductFlavourCMakeIdentifier() << "\" )" << newLine; 542 mo << " target_compile_options( ${BINARY_NAME} PRIVATE"; 543 544 for (auto& flag : cfg.getRecommendedCompilerWarningFlags()) 545 mo << " " << flag; 546 547 mo << ")" << newLine; 548 mo << "endif()" << newLine << newLine; 549 } 550 551 auto libraries = getAndroidLibraries(); 552 if (libraries.size() > 0) 553 { 554 for (auto& lib : libraries) 555 mo << "find_library(" << lib.toLowerCase().replaceCharacter (L' ', L'_') << " \"" << lib << "\")" << newLine; 556 557 mo << newLine; 558 } 559 560 libraries.addArray (userLibraries); 561 mo << "target_link_libraries( ${BINARY_NAME}"; 562 if (libraries.size() > 0) 563 { 564 mo << newLine << newLine; 565 566 for (auto& lib : libraries) 567 mo << " ${" << lib.toLowerCase().replaceCharacter (L' ', L'_') << "}" << newLine; 568 569 mo << " \"cpufeatures\"" << newLine; 570 } 571 572 if (useOboe) 573 mo << " \"oboe\"" << newLine; 574 575 mo << ")" << newLine; 576 }); 577 } 578 579 //============================================================================== getGradleSettingsFileContent()580 String getGradleSettingsFileContent() const 581 { 582 MemoryOutputStream mo; 583 mo.setNewLineString (getNewLineString()); 584 585 mo << "rootProject.name = " << "\'" << projectName << "\'" << newLine; 586 mo << (isLibrary() ? "include ':lib'" : "include ':app'"); 587 588 auto extraContent = androidGradleSettingsContent.get().toString(); 589 590 if (extraContent.isNotEmpty()) 591 mo << newLine << extraContent << newLine; 592 593 return mo.toString(); 594 } 595 getProjectBuildGradleFileContent()596 String getProjectBuildGradleFileContent() const 597 { 598 MemoryOutputStream mo; 599 mo.setNewLineString (getNewLineString()); 600 601 mo << "buildscript {" << newLine; 602 mo << " repositories {" << newLine; 603 mo << " google()" << newLine; 604 mo << " jcenter()" << newLine; 605 mo << " }" << newLine; 606 mo << " dependencies {" << newLine; 607 mo << " classpath 'com.android.tools.build:gradle:" << androidPluginVersion.get().toString() << "'" << newLine; 608 609 if (areRemoteNotificationsEnabled()) 610 mo << " classpath 'com.google.gms:google-services:4.0.1'" << newLine; 611 612 mo << " }" << newLine; 613 mo << "}" << newLine; 614 mo << "" << newLine; 615 mo << "allprojects {" << newLine; 616 mo << getAndroidProjectRepositories(); 617 mo << "}" << newLine; 618 619 return mo.toString(); 620 } 621 622 //============================================================================== getAppBuildGradleFileContent(const OwnedArray<LibraryModule> & modules)623 String getAppBuildGradleFileContent (const OwnedArray<LibraryModule>& modules) const 624 { 625 MemoryOutputStream mo; 626 mo.setNewLineString (getNewLineString()); 627 628 mo << "apply plugin: 'com.android." << (isLibrary() ? "library" : "application") << "'" << newLine << newLine; 629 630 mo << "android {" << newLine; 631 mo << " compileSdkVersion " << static_cast<int> (androidTargetSDK.get()) << newLine; 632 mo << " externalNativeBuild {" << newLine; 633 mo << " cmake {" << newLine; 634 mo << " path \"CMakeLists.txt\"" << newLine; 635 mo << " }" << newLine; 636 mo << " }" << newLine; 637 638 mo << getAndroidSigningConfig() << newLine; 639 mo << getAndroidDefaultConfig() << newLine; 640 mo << getAndroidBuildTypes() << newLine; 641 mo << getAndroidProductFlavours() << newLine; 642 mo << getAndroidVariantFilter() << newLine; 643 644 mo << getAndroidJavaSourceSets (modules) << newLine; 645 mo << getAndroidRepositories() << newLine; 646 mo << getAndroidDependencies() << newLine; 647 mo << androidCustomAppBuildGradleContent.get().toString() << newLine; 648 mo << getApplyPlugins() << newLine; 649 650 mo << "}" << newLine << newLine; 651 652 return mo.toString(); 653 } 654 getAndroidProductFlavours()655 String getAndroidProductFlavours() const 656 { 657 MemoryOutputStream mo; 658 mo.setNewLineString (getNewLineString()); 659 660 mo << " flavorDimensions \"default\"" << newLine; 661 mo << " productFlavors {" << newLine; 662 663 for (ConstConfigIterator config (*this); config.next();) 664 { 665 auto& cfg = dynamic_cast<const AndroidBuildConfiguration&> (*config); 666 667 mo << " " << cfg.getProductFlavourNameIdentifier() << " {" << newLine; 668 669 if (cfg.getArchitectures().isNotEmpty()) 670 { 671 mo << " ndk {" << newLine; 672 mo << " abiFilters " << toGradleList (StringArray::fromTokens (cfg.getArchitectures(), " ", "")) << newLine; 673 mo << " }" << newLine; 674 } 675 676 mo << " externalNativeBuild {" << newLine; 677 mo << " cmake {" << newLine; 678 679 if (getProject().getProjectType().isStaticLibrary()) 680 mo << " targets \"" << getNativeModuleBinaryName (cfg) << "\"" << newLine; 681 682 mo << " arguments " 683 << "\"-DJUCE_BUILD_CONFIGURATION=" << cfg.getProductFlavourCMakeIdentifier() << "\""; 684 685 mo << ", \"-DCMAKE_CXX_FLAGS_" << (cfg.isDebug() ? "DEBUG" : "RELEASE") 686 << "=-O" << cfg.getGCCOptimisationFlag(); 687 688 mo << "\"" 689 << ", \"-DCMAKE_C_FLAGS_" << (cfg.isDebug() ? "DEBUG" : "RELEASE") 690 << "=-O" << cfg.getGCCOptimisationFlag() 691 << "\"" << newLine; 692 693 mo << " }" << newLine; 694 mo << " }" << newLine << newLine; 695 mo << " dimension \"default\"" << newLine; 696 mo << " }" << newLine; 697 } 698 699 mo << " }" << newLine; 700 701 return mo.toString(); 702 } 703 getAndroidSigningConfig()704 String getAndroidSigningConfig() const 705 { 706 MemoryOutputStream mo; 707 mo.setNewLineString (getNewLineString()); 708 709 auto keyStoreFilePath = androidKeyStore.get().toString().replace ("${user.home}", "${System.properties['user.home']}") 710 .replace ("/", "${File.separator}"); 711 712 mo << " signingConfigs {" << newLine; 713 mo << " juceSigning {" << newLine; 714 mo << " storeFile file(\"" << keyStoreFilePath << "\")" << newLine; 715 mo << " storePassword \"" << androidKeyStorePass.get().toString() << "\"" << newLine; 716 mo << " keyAlias \"" << androidKeyAlias.get().toString() << "\"" << newLine; 717 mo << " keyPassword \"" << androidKeyAliasPass.get().toString() << "\"" << newLine; 718 mo << " storeType \"jks\"" << newLine; 719 mo << " }" << newLine; 720 mo << " }" << newLine; 721 722 return mo.toString(); 723 } 724 getAndroidDefaultConfig()725 String getAndroidDefaultConfig() const 726 { 727 auto bundleIdentifier = project.getBundleIdentifierString().toLowerCase(); 728 auto cmakeDefs = getCmakeDefinitions(); 729 auto minSdkVersion = static_cast<int> (androidMinimumSDK.get()); 730 auto targetSdkVersion = static_cast<int> (androidTargetSDK.get()); 731 732 MemoryOutputStream mo; 733 mo.setNewLineString (getNewLineString()); 734 735 mo << " defaultConfig {" << newLine; 736 737 if (! isLibrary()) 738 mo << " applicationId \"" << bundleIdentifier << "\"" << newLine; 739 740 mo << " minSdkVersion " << minSdkVersion << newLine; 741 mo << " targetSdkVersion " << targetSdkVersion << newLine; 742 743 mo << " externalNativeBuild {" << newLine; 744 mo << " cmake {" << newLine; 745 746 mo << " arguments " << cmakeDefs.joinIntoString (", ") << newLine; 747 748 mo << " }" << newLine; 749 mo << " }" << newLine; 750 mo << " }" << newLine; 751 752 return mo.toString(); 753 } 754 getAndroidBuildTypes()755 String getAndroidBuildTypes() const 756 { 757 MemoryOutputStream mo; 758 mo.setNewLineString (getNewLineString()); 759 760 mo << " buildTypes {" << newLine; 761 762 int numDebugConfigs = 0; 763 auto numConfigs = getNumConfigurations(); 764 for (int i = 0; i < numConfigs; ++i) 765 { 766 auto config = getConfiguration(i); 767 768 if (config->isDebug()) numDebugConfigs++; 769 770 if (numDebugConfigs > 1 || ((numConfigs - numDebugConfigs) > 1)) 771 continue; 772 773 mo << " " << (config->isDebug() ? "debug" : "release") << " {" << newLine; 774 mo << " initWith " << (config->isDebug() ? "debug" : "release") << newLine; 775 mo << " debuggable " << (config->isDebug() ? "true" : "false") << newLine; 776 mo << " jniDebuggable " << (config->isDebug() ? "true" : "false") << newLine; 777 mo << " signingConfig signingConfigs.juceSigning" << newLine; 778 779 mo << " }" << newLine; 780 } 781 mo << " }" << newLine; 782 783 return mo.toString(); 784 } 785 getAndroidVariantFilter()786 String getAndroidVariantFilter() const 787 { 788 MemoryOutputStream mo; 789 mo.setNewLineString (getNewLineString()); 790 791 mo << " variantFilter { variant ->" << newLine; 792 mo << " def names = variant.flavors*.name" << newLine; 793 794 for (ConstConfigIterator config (*this); config.next();) 795 { 796 auto& cfg = dynamic_cast<const AndroidBuildConfiguration&> (*config); 797 798 mo << " if (names.contains (\"" << cfg.getProductFlavourNameIdentifier() << "\")" << newLine; 799 mo << " && variant.buildType.name != \"" << (cfg.isDebug() ? "debug" : "release") << "\") {" << newLine; 800 mo << " setIgnore(true)" << newLine; 801 mo << " }" << newLine; 802 } 803 804 mo << " }" << newLine; 805 806 return mo.toString(); 807 } 808 getAndroidProjectRepositories()809 String getAndroidProjectRepositories() const 810 { 811 MemoryOutputStream mo; 812 mo.setNewLineString (getNewLineString()); 813 814 auto repositories = StringArray::fromLines (androidProjectRepositories.get().toString()); 815 816 if (areRemoteNotificationsEnabled()) 817 repositories.add ("maven { url \"https://maven.google.com\" }"); 818 819 mo << " repositories {" << newLine; 820 821 for (auto& r : repositories) 822 mo << " " << r << newLine; 823 824 mo << " }" << newLine; 825 826 return mo.toString(); 827 } 828 getAndroidRepositories()829 String getAndroidRepositories() const 830 { 831 MemoryOutputStream mo; 832 mo.setNewLineString (getNewLineString()); 833 834 auto repositories = StringArray::fromLines (androidRepositories.get().toString()); 835 836 mo << " repositories {" << newLine; 837 838 for (auto& r : repositories) 839 mo << " " << r << newLine; 840 841 mo << " }" << newLine; 842 843 return mo.toString(); 844 } 845 getAndroidDependencies()846 String getAndroidDependencies() const 847 { 848 MemoryOutputStream mo; 849 mo.setNewLineString (getNewLineString()); 850 851 mo << " dependencies {" << newLine; 852 853 for (auto& d : StringArray::fromLines (androidDependencies.get().toString())) 854 mo << " " << d << newLine; 855 856 for (auto& d : StringArray::fromLines (androidJavaLibs.get().toString())) 857 mo << " implementation files('libs/" << File (d).getFileName() << "')" << newLine; 858 859 if (isInAppBillingEnabled()) 860 mo << " implementation 'com.android.billingclient:billing:2.1.0'" << newLine; 861 862 if (areRemoteNotificationsEnabled()) 863 { 864 mo << " implementation 'com.google.firebase:firebase-core:16.0.1'" << newLine; 865 mo << " implementation 'com.google.firebase:firebase-messaging:17.6.0'" << newLine; 866 } 867 868 mo << " }" << newLine; 869 870 return mo.toString(); 871 } 872 getApplyPlugins()873 String getApplyPlugins() const 874 { 875 MemoryOutputStream mo; 876 mo.setNewLineString (getNewLineString()); 877 878 if (areRemoteNotificationsEnabled()) 879 mo << "apply plugin: 'com.google.gms.google-services'" << newLine; 880 881 return mo.toString(); 882 } 883 addModuleJavaFolderToSourceSet(StringArray & javaSourceSets,const File & source)884 void addModuleJavaFolderToSourceSet(StringArray& javaSourceSets, const File& source) const 885 { 886 if (source.isDirectory()) 887 { 888 auto appFolder = getTargetFolder().getChildFile ("app"); 889 890 build_tools::RelativePath relativePath (source, appFolder, build_tools::RelativePath::buildTargetFolder); 891 javaSourceSets.add (relativePath.toUnixStyle()); 892 } 893 } 894 addOptJavaFolderToSourceSetsForModule(StringArray & javaSourceSets,const OwnedArray<LibraryModule> & modules,const String & moduleID)895 void addOptJavaFolderToSourceSetsForModule (StringArray& javaSourceSets, 896 const OwnedArray<LibraryModule>& modules, 897 const String& moduleID) const 898 { 899 for (auto& m : modules) 900 { 901 if (m->getID() == moduleID) 902 { 903 auto javaFolder = m->getFolder().getChildFile ("native").getChildFile ("javaopt"); 904 addModuleJavaFolderToSourceSet (javaSourceSets, javaFolder.getChildFile ("app")); 905 return; 906 } 907 } 908 } 909 getAndroidJavaSourceSets(const OwnedArray<LibraryModule> & modules)910 String getAndroidJavaSourceSets (const OwnedArray<LibraryModule>& modules) const 911 { 912 auto javaSourceSets = getSourceSetArrayFor (androidAdditionalJavaFolders.get().toString()); 913 auto resourceSets = getSourceSetArrayFor (androidAdditionalResourceFolders.get().toString()); 914 915 for (auto* module : modules) 916 { 917 auto javaFolder = module->getFolder().getChildFile ("native").getChildFile ("javacore"); 918 919 addModuleJavaFolderToSourceSet (javaSourceSets, javaFolder.getChildFile ("init")); 920 921 if (! isLibrary()) 922 addModuleJavaFolderToSourceSet (javaSourceSets, javaFolder.getChildFile ("app")); 923 } 924 925 if (isUsingDefaultActivityClass() || isContentSharingEnabled()) 926 addOptJavaFolderToSourceSetsForModule (javaSourceSets, modules, "juce_gui_basics"); 927 928 if (areRemoteNotificationsEnabled()) 929 addOptJavaFolderToSourceSetsForModule (javaSourceSets, modules, "juce_gui_extra"); 930 931 if (isInAppBillingEnabled()) 932 addOptJavaFolderToSourceSetsForModule (javaSourceSets, modules, "juce_product_unlocking"); 933 934 MemoryOutputStream mo; 935 mo.setNewLineString (getNewLineString()); 936 937 mo << " sourceSets {" << newLine; 938 mo << getSourceSetStringFor ("main.java.srcDirs", javaSourceSets, getNewLineString()); 939 mo << newLine; 940 mo << getSourceSetStringFor ("main.res.srcDirs", resourceSets, getNewLineString()); 941 mo << " }" << newLine; 942 943 return mo.toString(); 944 } 945 getSourceSetArrayFor(const String & srcDirs)946 StringArray getSourceSetArrayFor (const String& srcDirs) const 947 { 948 StringArray sourceSets; 949 950 for (auto folder : StringArray::fromLines (srcDirs)) 951 { 952 if (File::isAbsolutePath (folder)) 953 { 954 sourceSets.add (folder); 955 } 956 else 957 { 958 auto appFolder = getTargetFolder().getChildFile ("app"); 959 960 auto relativePath = build_tools::RelativePath (folder, build_tools::RelativePath::projectFolder) 961 .rebased (getProject().getProjectFolder(), appFolder, 962 build_tools::RelativePath::buildTargetFolder); 963 964 sourceSets.add (relativePath.toUnixStyle()); 965 } 966 } 967 968 return sourceSets; 969 } 970 getSourceSetStringFor(const String & type,const StringArray & srcDirs,const String & newLineString)971 static String getSourceSetStringFor (const String& type, const StringArray& srcDirs, const String& newLineString) 972 { 973 String s; 974 975 s << " " << type << " +=" << newLine; 976 s << " ["; 977 978 bool isFirst = true; 979 980 for (auto sourceSet : srcDirs) 981 { 982 if (! isFirst) 983 s << "," << newLine << " "; 984 985 isFirst = false; 986 s << "\"" << sourceSet << "\""; 987 } 988 989 s << "]" << newLine; 990 991 return replaceLineFeeds (s, newLineString); 992 } 993 994 //============================================================================== getLocalPropertiesFileContent()995 String getLocalPropertiesFileContent() const 996 { 997 String props; 998 999 props << "sdk.dir=" << sanitisePath (getAppSettings().getStoredPath (Ids::androidSDKPath, TargetOS::getThisOS()).get().toString()) << newLine; 1000 1001 return replaceLineFeeds (props, getNewLineString()); 1002 } 1003 getGradleWrapperPropertiesFileContent()1004 String getGradleWrapperPropertiesFileContent() const 1005 { 1006 String props; 1007 1008 props << "distributionUrl=https\\://services.gradle.org/distributions/gradle-" 1009 << gradleVersion.get().toString() << "-all.zip"; 1010 1011 return props; 1012 } 1013 1014 //============================================================================== createBaseExporterProperties(PropertyListBuilder & props)1015 void createBaseExporterProperties (PropertyListBuilder& props) 1016 { 1017 props.add (new TextPropertyComponent (androidAdditionalJavaFolders, "Java Source code folders", 32768, true), 1018 "Folders inside which additional java source files can be found (one per line). For example, if you " 1019 "are using your own Activity you should place the java files for this into a folder and add the folder " 1020 "path to this field."); 1021 1022 props.add (new TextPropertyComponent (androidAdditionalResourceFolders, "Resource folders", 32768, true), 1023 "Folders inside which additional resource files can be found (one per line). For example, if you " 1024 "want to add your own layout xml files then you should place a layout xml file inside a folder and add " 1025 "the folder path to this field."); 1026 1027 props.add (new TextPropertyComponent (androidJavaLibs, "Java libraries to include", 32768, true), 1028 "Java libs (JAR files) (one per line). These will be copied to app/libs folder and \"implementation files\" " 1029 "dependency will be automatically added to module \"dependencies\" section for each library, so do " 1030 "not add the dependency yourself."); 1031 1032 props.add (new TextPropertyComponent (androidProjectRepositories, "Project Repositories", 32768, true), 1033 "Custom project repositories (one per line). These will be used in project-level gradle file " 1034 "\"allprojects { repositories {\" section instead of default ones."); 1035 1036 props.add (new TextPropertyComponent (androidRepositories, "Module Repositories", 32768, true), 1037 "Module repositories (one per line). These will be added to module-level gradle file repositories section. "); 1038 1039 props.add (new TextPropertyComponent (androidDependencies, "Module Dependencies", 32768, true), 1040 "Module dependencies (one per line). These will be added to module-level gradle file \"dependencies\" section. " 1041 "If adding any java libs in \"Java libraries to include\" setting, do not add them here as " 1042 "they will be added automatically."); 1043 1044 props.add (new TextPropertyComponent (androidCustomAppBuildGradleContent, "Extra module's build.gradle content", 32768, true), 1045 "Additional content to be appended to module's build.gradle inside android { section. "); 1046 1047 props.add (new TextPropertyComponent (androidGradleSettingsContent, "Custom gradle.settings content", 32768, true), 1048 "You can customize the content of settings.gradle here"); 1049 1050 props.add (new ChoicePropertyComponent (androidScreenOrientation, "Screen Orientation", 1051 { "Portrait and Landscape", "Portrait", "Landscape" }, 1052 { "unspecified", "portrait", "landscape" }), 1053 "The screen orientations that this app should support"); 1054 1055 props.add (new TextPropertyComponent (androidCustomActivityClass, "Custom Android Activity", 256, false), 1056 "If not empty, specifies the Android Activity class name stored in the app's manifest which " 1057 "should be used instead of Android's default Activity. If you specify a custom Activity " 1058 "then you should implement onNewIntent() function like the one in com.rmsl.juce.JuceActivity, if " 1059 "you wish to be able to handle push notification events."); 1060 1061 props.add (new TextPropertyComponent (androidCustomApplicationClass, "Custom Android Application", 256, false), 1062 "If not empty, specifies the Android Application class name stored in the app's manifest which " 1063 "should be used instead of JUCE's default JuceApp class. If you specify a custom App then you must " 1064 "call com.rmsl.juce.Java.initialiseJUCE somewhere in your code before calling any JUCE functions."); 1065 1066 props.add (new TextPropertyComponent (androidVersionCode, "Android Version Code", 32, false), 1067 "An integer value that represents the version of the application code, relative to other versions."); 1068 1069 props.add (new TextPropertyComponent (androidMinimumSDK, "Minimum SDK Version", 32, false), 1070 "The number of the minimum version of the Android SDK that the app requires (must be 16 or higher)."); 1071 1072 props.add (new TextPropertyComponent (androidTargetSDK, "Target SDK Version", 32, false), 1073 "The number of the version of the Android SDK that the app is targeting."); 1074 1075 props.add (new TextPropertyComponent (androidExtraAssetsFolder, "Extra Android Assets", 256, false), 1076 "A path to a folder (relative to the project folder) which contains extra android assets."); 1077 } 1078 1079 //============================================================================== createManifestExporterProperties(PropertyListBuilder & props)1080 void createManifestExporterProperties (PropertyListBuilder& props) 1081 { 1082 props.add (new TextPropertyComponent (androidOboeRepositoryPath, "Custom Oboe Repository", 2048, false), 1083 "Path to the root of Oboe repository. This path can be absolute, or relative to the build directory. " 1084 "Make sure to point Oboe repository to commit with SHA c5c3cc17f78974bf005bf33a2de1a093ac55cc07 before building. " 1085 "Leave blank to use the version of Oboe distributed with JUCE."); 1086 1087 props.add (new ChoicePropertyComponent (androidInternetNeeded, "Internet Access"), 1088 "If enabled, this will set the android.permission.INTERNET flag in the manifest."); 1089 1090 props.add (new ChoicePropertyComponent (androidMicNeeded, "Audio Input Required"), 1091 "If enabled, this will set the android.permission.RECORD_AUDIO flag in the manifest."); 1092 1093 props.add (new ChoicePropertyComponent (androidCameraNeeded, "Camera Required"), 1094 "If enabled, this will set the android.permission.CAMERA flag in the manifest."); 1095 1096 props.add (new ChoicePropertyComponent (androidBluetoothNeeded, "Bluetooth Permissions Required"), 1097 "If enabled, this will set the android.permission.BLUETOOTH and android.permission.BLUETOOTH_ADMIN flag in the manifest. This is required for Bluetooth MIDI on Android."); 1098 1099 props.add (new ChoicePropertyComponent (androidExternalReadPermission, "Read From External Storage"), 1100 "If enabled, this will set the android.permission.READ_EXTERNAL_STORAGE flag in the manifest."); 1101 1102 props.add (new ChoicePropertyComponent (androidExternalWritePermission, "Write to External Storage"), 1103 "If enabled, this will set the android.permission.WRITE_EXTERNAL_STORAGE flag in the manifest."); 1104 1105 props.add (new ChoicePropertyComponent (androidInAppBillingPermission, "In-App Billing"), 1106 "If enabled, this will set the com.android.vending.BILLING flag in the manifest."); 1107 1108 props.add (new ChoicePropertyComponent (androidVibratePermission, "Vibrate"), 1109 "If enabled, this will set the android.permission.VIBRATE flag in the manifest."); 1110 1111 props.add (new ChoicePropertyComponent (androidEnableContentSharing, "Content Sharing"), 1112 "If enabled, your app will be able to share content with other apps."); 1113 1114 props.add (new TextPropertyComponent (androidOtherPermissions, "Custom Permissions", 2048, false), 1115 "A space-separated list of other permission flags that should be added to the manifest."); 1116 1117 props.add (new ChoicePropertyComponent (androidPushNotifications, "Push Notifications Capability"), 1118 "Enable this to grant your app the capability to receive push notifications."); 1119 1120 props.add (new ChoicePropertyComponentWithEnablement (androidEnableRemoteNotifications, androidPushNotifications, "Remote Notifications"), 1121 "Enable to be able to send remote notifications to devices running your app (min API level 14). Enable the \"Push Notifications Capability\" " 1122 "setting, provide Remote Notifications Config File, configure your app in Firebase Console and ensure you have the latest Google Repository " 1123 "in Android Studio's SDK Manager."); 1124 1125 props.add (new TextPropertyComponent (androidRemoteNotificationsConfigFile.getPropertyAsValue(), "Remote Notifications Config File", 2048, false), 1126 "Path to google-services.json file. This will be the file provided by Firebase when creating a new app in Firebase console."); 1127 1128 props.add (new TextPropertyComponent (androidManifestCustomXmlElements, "Custom Manifest XML Content", 8192, true), 1129 "You can specify custom AndroidManifest.xml content overriding the default one generated by Projucer. " 1130 "Projucer will automatically create any missing and required XML elements and attributes " 1131 "and merge them into your custom content."); 1132 } 1133 1134 //============================================================================== createCodeSigningExporterProperties(PropertyListBuilder & props)1135 void createCodeSigningExporterProperties (PropertyListBuilder& props) 1136 { 1137 props.add (new TextPropertyComponent (androidKeyStore, "Key Signing: key.store", 2048, false), 1138 "The key.store value, used when signing the release package."); 1139 1140 props.add (new TextPropertyComponent (androidKeyStorePass, "Key Signing: key.store.password", 2048, false), 1141 "The key.store password, used when signing the release package."); 1142 1143 props.add (new TextPropertyComponent (androidKeyAlias, "Key Signing: key.alias", 2048, false), 1144 "The key.alias value, used when signing the release package."); 1145 1146 props.add (new TextPropertyComponent (androidKeyAliasPass, "Key Signing: key.alias.password", 2048, false), 1147 "The key.alias password, used when signing the release package."); 1148 } 1149 1150 //============================================================================== createOtherExporterProperties(PropertyListBuilder & props)1151 void createOtherExporterProperties (PropertyListBuilder& props) 1152 { 1153 props.add (new TextPropertyComponent (androidTheme, "Android Theme", 256, false), 1154 "E.g. @android:style/Theme.NoTitleBar or leave blank for default"); 1155 } 1156 1157 //============================================================================== copyAdditionalJavaLibs(const File & targetFolder)1158 void copyAdditionalJavaLibs (const File& targetFolder) const 1159 { 1160 auto libFolder = targetFolder.getChildFile ("libs"); 1161 libFolder.createDirectory(); 1162 1163 auto libPaths = StringArray::fromLines (androidJavaLibs.get().toString()); 1164 1165 for (auto& p : libPaths) 1166 { 1167 auto f = getTargetFolder().getChildFile (p); 1168 1169 // Is the path to the java lib correct? 1170 jassert (f.existsAsFile()); 1171 1172 f.copyFileTo (libFolder.getChildFile (f.getFileName())); 1173 } 1174 } 1175 copyExtraResourceFiles()1176 void copyExtraResourceFiles() const 1177 { 1178 for (ConstConfigIterator config (*this); config.next();) 1179 { 1180 auto& cfg = dynamic_cast<const AndroidBuildConfiguration&> (*config); 1181 String cfgPath = cfg.isDebug() ? "app/src/debug" : "app/src/release"; 1182 String xmlValuesPath = cfg.isDebug() ? "app/src/debug/res/values" : "app/src/release/res/values"; 1183 String drawablesPath = cfg.isDebug() ? "app/src/debug/res" : "app/src/release/res"; 1184 String rawPath = cfg.isDebug() ? "app/src/debug/res/raw" : "app/src/release/res/raw"; 1185 1186 copyExtraResourceFiles (cfg.getAdditionalXmlResources(), xmlValuesPath); 1187 copyExtraResourceFiles (cfg.getAdditionalDrawableResources(), drawablesPath); 1188 copyExtraResourceFiles (cfg.getAdditionalRawResources(), rawPath); 1189 1190 if (areRemoteNotificationsEnabled()) 1191 { 1192 auto remoteNotifsConfigFilePath = cfg.getRemoteNotifsConfigFile(); 1193 1194 if (remoteNotifsConfigFilePath.isEmpty()) 1195 remoteNotifsConfigFilePath = androidRemoteNotificationsConfigFile.get().toString(); 1196 1197 File file (getProject().getFile().getChildFile (remoteNotifsConfigFilePath)); 1198 // Settings file must be present for remote notifications to work and it must be called google-services.json. 1199 jassert (file.existsAsFile() && file.getFileName() == "google-services.json"); 1200 1201 copyExtraResourceFiles (remoteNotifsConfigFilePath, cfgPath); 1202 } 1203 } 1204 } 1205 copyExtraResourceFiles(const String & resources,const String & dstRelativePath)1206 void copyExtraResourceFiles (const String& resources, const String& dstRelativePath) const 1207 { 1208 auto resourcePaths = StringArray::fromTokens (resources, true); 1209 1210 auto parentFolder = getTargetFolder().getChildFile (dstRelativePath); 1211 1212 parentFolder.createDirectory(); 1213 1214 for (auto& path : resourcePaths) 1215 { 1216 auto file = getProject().getFile().getChildFile (path); 1217 1218 jassert (file.exists()); 1219 1220 if (file.exists()) 1221 file.copyFileTo (parentFolder.getChildFile (file.getFileName())); 1222 } 1223 } 1224 1225 //============================================================================== getActivityClassString()1226 String getActivityClassString() const 1227 { 1228 auto customActivityClass = androidCustomActivityClass.get().toString(); 1229 1230 if (customActivityClass.isNotEmpty()) 1231 return customActivityClass; 1232 1233 return arePushNotificationsEnabled() ? getDefaultActivityClass() : "android.app.Activity"; 1234 } 1235 getApplicationClassString()1236 String getApplicationClassString() const { return androidCustomApplicationClass.get(); } getJNIActivityClassName()1237 String getJNIActivityClassName() const { return getActivityClassString().replaceCharacter ('.', '/'); } 1238 isUsingDefaultActivityClass()1239 bool isUsingDefaultActivityClass() const { return getActivityClassString() == getDefaultActivityClass(); } 1240 1241 //============================================================================== arePushNotificationsEnabled()1242 bool arePushNotificationsEnabled() const 1243 { 1244 return project.getEnabledModules().isModuleEnabled ("juce_gui_extra") 1245 && androidPushNotifications.get(); 1246 } 1247 areRemoteNotificationsEnabled()1248 bool areRemoteNotificationsEnabled() const 1249 { 1250 return arePushNotificationsEnabled() 1251 && androidEnableRemoteNotifications.get(); 1252 } 1253 isInAppBillingEnabled()1254 bool isInAppBillingEnabled() const 1255 { 1256 return project.getEnabledModules().isModuleEnabled ("juce_product_unlocking") 1257 && androidInAppBillingPermission.get(); 1258 } 1259 isContentSharingEnabled()1260 bool isContentSharingEnabled() const 1261 { 1262 return project.getEnabledModules().isModuleEnabled ("juce_gui_basics") 1263 && androidEnableContentSharing.get(); 1264 } 1265 1266 //============================================================================== getNativeModuleBinaryName(const AndroidBuildConfiguration & config)1267 String getNativeModuleBinaryName (const AndroidBuildConfiguration& config) const 1268 { 1269 return (isLibrary() ? File::createLegalFileName (config.getTargetBinaryNameString().trim()) : "juce_jni"); 1270 } 1271 getAppPlatform()1272 String getAppPlatform() const 1273 { 1274 auto ndkVersion = static_cast<int> (androidMinimumSDK.get()); 1275 if (ndkVersion == 9) 1276 ndkVersion = 10; // (doesn't seem to be a version '9') 1277 1278 return "android-" + String (ndkVersion); 1279 } 1280 1281 //============================================================================== writeStringsXML(const File & folder)1282 void writeStringsXML (const File& folder) const 1283 { 1284 for (ConstConfigIterator config (*this); config.next();) 1285 { 1286 auto& cfg = dynamic_cast<const AndroidBuildConfiguration&> (*config); 1287 1288 String customStringsXmlContent ("<resources>\n"); 1289 customStringsXmlContent << "<string name=\"app_name\">" << projectName << "</string>\n"; 1290 customStringsXmlContent << cfg.getCustomStringsXml(); 1291 customStringsXmlContent << "\n</resources>"; 1292 1293 if (auto strings = parseXML (customStringsXmlContent)) 1294 { 1295 String dir = cfg.isDebug() ? "debug" : "release"; 1296 String subPath = "app/src/" + dir + "/res/values/string.xml"; 1297 1298 writeXmlOrThrow (*strings, folder.getChildFile (subPath), "utf-8", 100, true); 1299 } 1300 else 1301 { 1302 jassertfalse; // needs handling? 1303 } 1304 } 1305 } 1306 writeAndroidManifest(const File & folder)1307 void writeAndroidManifest (const File& folder) const 1308 { 1309 std::unique_ptr<XmlElement> manifest (createManifestXML()); 1310 1311 writeXmlOrThrow (*manifest, folder.getChildFile ("src/main/AndroidManifest.xml"), "utf-8", 100, true); 1312 } 1313 writeIcon(const File & file,const Image & im)1314 void writeIcon (const File& file, const Image& im) const 1315 { 1316 if (im.isValid()) 1317 { 1318 createDirectoryOrThrow (file.getParentDirectory()); 1319 1320 build_tools::writeStreamToFile (file, [&] (MemoryOutputStream& mo) 1321 { 1322 mo.setNewLineString (getNewLineString()); 1323 1324 PNGImageFormat png; 1325 1326 if (! png.writeImageToStream (im, mo)) 1327 throw build_tools::SaveError ("Can't generate Android icon file"); 1328 }); 1329 } 1330 } 1331 writeIcons(const File & folder)1332 void writeIcons (const File& folder) const 1333 { 1334 const auto icons = getIcons(); 1335 1336 if (icons.big != nullptr && icons.small != nullptr) 1337 { 1338 auto step = jmax (icons.big->getWidth(), icons.big->getHeight()) / 8; 1339 writeIcon (folder.getChildFile ("drawable-xhdpi/icon.png"), build_tools::getBestIconForSize (icons, step * 8, false)); 1340 writeIcon (folder.getChildFile ("drawable-hdpi/icon.png"), build_tools::getBestIconForSize (icons, step * 6, false)); 1341 writeIcon (folder.getChildFile ("drawable-mdpi/icon.png"), build_tools::getBestIconForSize (icons, step * 4, false)); 1342 writeIcon (folder.getChildFile ("drawable-ldpi/icon.png"), build_tools::getBestIconForSize (icons, step * 3, false)); 1343 } 1344 else if (auto* icon = (icons.big != nullptr ? icons.big.get() : icons.small.get())) 1345 { 1346 writeIcon (folder.getChildFile ("drawable-mdpi/icon.png"), build_tools::rescaleImageForIcon (*icon, icon->getWidth())); 1347 } 1348 } 1349 writeAppIcons(const File & folder)1350 void writeAppIcons (const File& folder) const 1351 { 1352 writeIcons (folder.getChildFile ("app/src/main/res/")); 1353 } 1354 sanitisePath(String path)1355 static String sanitisePath (String path) 1356 { 1357 return expandHomeFolderToken (path).replace ("\\", "\\\\"); 1358 } 1359 expandHomeFolderToken(const String & path)1360 static String expandHomeFolderToken (const String& path) 1361 { 1362 auto homeFolder = File::getSpecialLocation (File::userHomeDirectory).getFullPathName(); 1363 1364 return path.replace ("${user.home}", homeFolder) 1365 .replace ("~", homeFolder); 1366 } 1367 1368 //============================================================================== addCompileUnits(const Project::Item & projectItem,MemoryOutputStream & mo,Array<build_tools::RelativePath> & excludeFromBuild,Array<std::pair<build_tools::RelativePath,String>> & extraCompilerFlags)1369 void addCompileUnits (const Project::Item& projectItem, MemoryOutputStream& mo, 1370 Array<build_tools::RelativePath>& excludeFromBuild, Array<std::pair<build_tools::RelativePath, String>>& extraCompilerFlags) const 1371 { 1372 if (projectItem.isGroup()) 1373 { 1374 for (int i = 0; i < projectItem.getNumChildren(); ++i) 1375 addCompileUnits (projectItem.getChild (i), mo, excludeFromBuild, extraCompilerFlags); 1376 } 1377 else if (projectItem.shouldBeAddedToTargetProject() && projectItem.shouldBeAddedToTargetExporter (*this)) 1378 { 1379 auto f = projectItem.getFile(); 1380 build_tools::RelativePath file (f, getTargetFolder().getChildFile ("app"), build_tools::RelativePath::buildTargetFolder); 1381 1382 auto targetType = getProject().getTargetTypeFromFilePath (f, true); 1383 1384 mo << " \"" << file.toUnixStyle() << "\"" << newLine; 1385 1386 if ((! projectItem.shouldBeCompiled()) || (! shouldFileBeCompiledByDefault (f)) 1387 || (getProject().isAudioPluginProject() 1388 && targetType != build_tools::ProjectType::Target::SharedCodeTarget 1389 && targetType != build_tools::ProjectType::Target::StandalonePlugIn)) 1390 { 1391 excludeFromBuild.add (file); 1392 } 1393 else 1394 { 1395 auto extraFlags = compilerFlagSchemesMap[projectItem.getCompilerFlagSchemeString()].get().toString(); 1396 1397 if (extraFlags.isNotEmpty()) 1398 extraCompilerFlags.add ({ file, extraFlags }); 1399 } 1400 } 1401 } 1402 addCompileUnits(MemoryOutputStream & mo,Array<build_tools::RelativePath> & excludeFromBuild,Array<std::pair<build_tools::RelativePath,String>> & extraCompilerFlags)1403 void addCompileUnits (MemoryOutputStream& mo, Array<build_tools::RelativePath>& excludeFromBuild, 1404 Array<std::pair<build_tools::RelativePath, String>>& extraCompilerFlags) const 1405 { 1406 for (int i = 0; i < getAllGroups().size(); ++i) 1407 addCompileUnits (getAllGroups().getReference(i), mo, excludeFromBuild, extraCompilerFlags); 1408 } 1409 1410 //============================================================================== getCmakeDefinitions()1411 StringArray getCmakeDefinitions() const 1412 { 1413 auto toolchain = gradleToolchain.get().toString(); 1414 bool isClang = (toolchain == "clang"); 1415 1416 StringArray cmakeArgs; 1417 1418 cmakeArgs.add ("\"-DANDROID_TOOLCHAIN=" + toolchain + "\""); 1419 cmakeArgs.add ("\"-DANDROID_PLATFORM=" + getAppPlatform() + "\""); 1420 cmakeArgs.add (String ("\"-DANDROID_STL=") + (isClang ? "c++_static" : "gnustl_static") + "\""); 1421 cmakeArgs.add ("\"-DANDROID_CPP_FEATURES=exceptions rtti\""); 1422 cmakeArgs.add ("\"-DANDROID_ARM_MODE=arm\""); 1423 cmakeArgs.add ("\"-DANDROID_ARM_NEON=TRUE\""); 1424 1425 auto cppStandard = [this] 1426 { 1427 auto projectStandard = project.getCppStandardString(); 1428 1429 if (projectStandard == "latest") 1430 return String ("17"); 1431 1432 return projectStandard; 1433 }(); 1434 1435 cmakeArgs.add ("\"-DCMAKE_CXX_STANDARD=" + cppStandard + "\""); 1436 cmakeArgs.add ("\"-DCMAKE_CXX_EXTENSIONS=" + String (shouldUseGNUExtensions() ? "ON" : "OFF") + "\""); 1437 1438 return cmakeArgs; 1439 } 1440 1441 //============================================================================== getAndroidCompilerFlags()1442 StringArray getAndroidCompilerFlags() const 1443 { 1444 StringArray cFlags; 1445 cFlags.add ("\"-fsigned-char\""); 1446 1447 return cFlags; 1448 } 1449 getProjectCompilerFlags()1450 StringArray getProjectCompilerFlags() const 1451 { 1452 auto cFlags = getAndroidCompilerFlags(); 1453 cFlags.addArray (getEscapedFlags (StringArray::fromTokens (getExtraCompilerFlagsString(), true))); 1454 return cFlags; 1455 } 1456 1457 //============================================================================== getAndroidPreprocessorDefs()1458 StringPairArray getAndroidPreprocessorDefs() const 1459 { 1460 StringPairArray defines; 1461 1462 defines.set ("JUCE_ANDROID", "1"); 1463 defines.set ("JUCE_ANDROID_API_VERSION", androidMinimumSDK.get()); 1464 1465 if (arePushNotificationsEnabled()) 1466 { 1467 defines.set ("JUCE_PUSH_NOTIFICATIONS", "1"); 1468 defines.set ("JUCE_PUSH_NOTIFICATIONS_ACTIVITY", getJNIActivityClassName().quoted()); 1469 } 1470 1471 if (isInAppBillingEnabled()) 1472 defines.set ("JUCE_IN_APP_PURCHASES", "1"); 1473 1474 if (isContentSharingEnabled()) 1475 defines.set ("JUCE_CONTENT_SHARING", "1"); 1476 1477 if (supportsGLv3()) 1478 defines.set ("JUCE_ANDROID_GL_ES_VERSION_3_0", "1"); 1479 1480 if (areRemoteNotificationsEnabled()) 1481 { 1482 defines.set ("JUCE_FIREBASE_INSTANCE_ID_SERVICE_CLASSNAME", "com_rmsl_juce_JuceFirebaseInstanceIdService"); 1483 defines.set ("JUCE_FIREBASE_MESSAGING_SERVICE_CLASSNAME", "com_rmsl_juce_JuceFirebaseMessagingService"); 1484 } 1485 1486 return defines; 1487 } 1488 getProjectPreprocessorDefs()1489 StringPairArray getProjectPreprocessorDefs() const 1490 { 1491 auto defines = getAndroidPreprocessorDefs(); 1492 1493 return mergePreprocessorDefs (defines, getAllPreprocessorDefs()); 1494 } 1495 getConfigPreprocessorDefs(const BuildConfiguration & config)1496 StringPairArray getConfigPreprocessorDefs (const BuildConfiguration& config) const 1497 { 1498 auto cfgDefines = getAllPreprocessorDefs (config, build_tools::ProjectType::Target::unspecified); 1499 1500 if (config.isDebug()) 1501 { 1502 cfgDefines.set ("DEBUG", "1"); 1503 cfgDefines.set ("_DEBUG", "1"); 1504 } 1505 else 1506 { 1507 cfgDefines.set ("NDEBUG", "1"); 1508 } 1509 1510 return cfgDefines; 1511 } 1512 1513 //============================================================================== getAndroidLibraries()1514 StringArray getAndroidLibraries() const 1515 { 1516 StringArray libraries; 1517 1518 libraries.add ("log"); 1519 libraries.add ("android"); 1520 libraries.add (supportsGLv3() ? "GLESv3" : "GLESv2"); 1521 libraries.add ("EGL"); 1522 1523 return libraries; 1524 } 1525 1526 //============================================================================== getHeaderSearchPaths(const BuildConfiguration & config)1527 StringArray getHeaderSearchPaths (const BuildConfiguration& config) const 1528 { 1529 auto paths = extraSearchPaths; 1530 paths.addArray (config.getHeaderSearchPaths()); 1531 paths = getCleanedStringArray (paths); 1532 return paths; 1533 } 1534 1535 //============================================================================== escapeDirectoryForCmake(const String & path)1536 String escapeDirectoryForCmake (const String& path) const 1537 { 1538 auto relative = 1539 build_tools::RelativePath (path, build_tools::RelativePath::buildTargetFolder) 1540 .rebased (getTargetFolder(), getTargetFolder().getChildFile ("app"), build_tools::RelativePath::buildTargetFolder); 1541 1542 return relative.toUnixStyle(); 1543 } 1544 1545 void writeCmakePathLines (MemoryOutputStream& mo, const String& prefix, const String& firstLine, const StringArray& paths, 1546 const String& suffix = ")") const 1547 { 1548 if (paths.size() > 0) 1549 { 1550 mo << prefix << firstLine << newLine; 1551 1552 for (auto& path : paths) 1553 mo << prefix << " \"" << escapeDirectoryForCmake (path) << "\"" << newLine; 1554 1555 mo << prefix << suffix << newLine << newLine; 1556 } 1557 } 1558 getEscapedPreprocessorDefs(const StringPairArray & defs)1559 static StringArray getEscapedPreprocessorDefs (const StringPairArray& defs) 1560 { 1561 StringArray escapedDefs; 1562 1563 for (int i = 0; i < defs.size(); ++i) 1564 { 1565 auto escaped = "\"-D" + defs.getAllKeys()[i]; 1566 auto value = defs.getAllValues()[i]; 1567 1568 if (value.isNotEmpty()) 1569 { 1570 value = value.replace ("\"", "\\\""); 1571 1572 if (value.containsChar (L' ') && ! value.startsWith ("\\\"") && ! value.endsWith ("\\\"")) 1573 value = "\\\"" + value + "\\\""; 1574 1575 escaped += ("=" + value); 1576 } 1577 1578 escapedDefs.add (escaped + "\""); 1579 } 1580 1581 return escapedDefs; 1582 } 1583 getEscapedFlags(const StringArray & flags)1584 static StringArray getEscapedFlags (const StringArray& flags) 1585 { 1586 StringArray escaped; 1587 1588 for (auto& flag : flags) 1589 escaped.add ("\"" + flag + "\""); 1590 1591 return escaped; 1592 } 1593 1594 //============================================================================== createManifestXML()1595 std::unique_ptr<XmlElement> createManifestXML() const 1596 { 1597 auto manifest = createManifestElement(); 1598 1599 createSupportsScreensElement (*manifest); 1600 createPermissionElements (*manifest); 1601 createOpenGlFeatureElement (*manifest); 1602 1603 if (! isLibrary()) 1604 { 1605 auto* app = createApplicationElement (*manifest); 1606 1607 auto* act = createActivityElement (*app); 1608 createIntentElement (*act); 1609 1610 createServiceElements (*app); 1611 createProviderElement (*app); 1612 } 1613 1614 return manifest; 1615 } 1616 createManifestElement()1617 std::unique_ptr<XmlElement> createManifestElement() const 1618 { 1619 auto manifest = parseXML (androidManifestCustomXmlElements.get()); 1620 1621 if (manifest == nullptr) 1622 manifest = std::make_unique<XmlElement> ("manifest"); 1623 1624 setAttributeIfNotPresent (*manifest, "xmlns:android", "http://schemas.android.com/apk/res/android"); 1625 setAttributeIfNotPresent (*manifest, "android:versionCode", androidVersionCode.get()); 1626 setAttributeIfNotPresent (*manifest, "android:versionName", project.getVersionString()); 1627 setAttributeIfNotPresent (*manifest, "package", project.getBundleIdentifierString()); 1628 1629 return manifest; 1630 } 1631 createSupportsScreensElement(XmlElement & manifest)1632 void createSupportsScreensElement (XmlElement& manifest) const 1633 { 1634 if (! isLibrary()) 1635 { 1636 if (manifest.getChildByName ("supports-screens") == nullptr) 1637 { 1638 auto* screens = manifest.createNewChildElement ("supports-screens"); 1639 screens->setAttribute ("android:smallScreens", "true"); 1640 screens->setAttribute ("android:normalScreens", "true"); 1641 screens->setAttribute ("android:largeScreens", "true"); 1642 screens->setAttribute ("android:anyDensity", "true"); 1643 screens->setAttribute ("android:xlargeScreens", "true"); 1644 } 1645 } 1646 } 1647 createPermissionElements(XmlElement & manifest)1648 void createPermissionElements (XmlElement& manifest) const 1649 { 1650 auto permissions = getPermissionsRequired(); 1651 1652 for (auto* child : manifest.getChildWithTagNameIterator ("uses-permission")) 1653 { 1654 permissions.removeString (child->getStringAttribute ("android:name"), false); 1655 } 1656 1657 for (int i = permissions.size(); --i >= 0;) 1658 manifest.createNewChildElement ("uses-permission")->setAttribute ("android:name", permissions[i]); 1659 } 1660 createOpenGlFeatureElement(XmlElement & manifest)1661 void createOpenGlFeatureElement (XmlElement& manifest) const 1662 { 1663 if (project.getEnabledModules().isModuleEnabled ("juce_opengl")) 1664 { 1665 XmlElement* glVersion = nullptr; 1666 1667 for (auto* child : manifest.getChildWithTagNameIterator ("uses-feature")) 1668 { 1669 if (child->getStringAttribute ("android:glEsVersion").isNotEmpty()) 1670 { 1671 glVersion = child; 1672 break; 1673 } 1674 } 1675 1676 if (glVersion == nullptr) 1677 glVersion = manifest.createNewChildElement ("uses-feature"); 1678 1679 setAttributeIfNotPresent (*glVersion, "android:glEsVersion", (static_cast<int> (androidMinimumSDK.get()) >= 18 ? "0x00030000" : "0x00020000")); 1680 setAttributeIfNotPresent (*glVersion, "android:required", "true"); 1681 } 1682 } 1683 createApplicationElement(XmlElement & manifest)1684 XmlElement* createApplicationElement (XmlElement& manifest) const 1685 { 1686 auto* app = getOrCreateChildWithName (manifest, "application"); 1687 setAttributeIfNotPresent (*app, "android:label", "@string/app_name"); 1688 setAttributeIfNotPresent (*app, "android:name", getApplicationClassString()); 1689 1690 if (androidTheme.get().toString().isNotEmpty()) 1691 setAttributeIfNotPresent (*app, "android:theme", androidTheme.get()); 1692 1693 if (! app->hasAttribute ("android:icon")) 1694 { 1695 std::unique_ptr<Drawable> bigIcon (getBigIcon()), smallIcon (getSmallIcon()); 1696 1697 if (bigIcon != nullptr || smallIcon != nullptr) 1698 app->setAttribute ("android:icon", "@drawable/icon"); 1699 } 1700 1701 if (static_cast<int> (androidMinimumSDK.get()) >= 11) 1702 { 1703 if (! app->hasAttribute ("android:hardwareAccelerated")) 1704 app->setAttribute ("android:hardwareAccelerated", "false"); // (using the 2D acceleration slows down openGL) 1705 } 1706 else 1707 { 1708 app->removeAttribute ("android:hardwareAccelerated"); 1709 } 1710 1711 return app; 1712 } 1713 createActivityElement(XmlElement & application)1714 XmlElement* createActivityElement (XmlElement& application) const 1715 { 1716 auto* act = getOrCreateChildWithName (application, "activity"); 1717 1718 setAttributeIfNotPresent (*act, "android:name", getActivityClassString()); 1719 setAttributeIfNotPresent (*act, "android:label", "@string/app_name"); 1720 1721 if (! act->hasAttribute ("android:configChanges")) 1722 { 1723 String configChanges ("keyboardHidden|orientation"); 1724 if (static_cast<int> (androidMinimumSDK.get()) >= 13) 1725 configChanges += "|screenSize"; 1726 1727 act->setAttribute ("android:configChanges", configChanges); 1728 } 1729 else 1730 { 1731 auto configChanges = act->getStringAttribute ("android:configChanges"); 1732 1733 if (static_cast<int> (androidMinimumSDK.get()) < 13 && configChanges.contains ("screenSize")) 1734 { 1735 configChanges = configChanges.replace ("|screenSize", "") 1736 .replace ("screenSize|", "") 1737 .replace ("screenSize", ""); 1738 1739 act->setAttribute ("android:configChanges", configChanges); 1740 } 1741 } 1742 1743 if (androidScreenOrientation.get() == "landscape") 1744 { 1745 String landscapeString = static_cast<int> (androidMinimumSDK.get()) < 9 1746 ? "landscape" 1747 : (static_cast<int> (androidMinimumSDK.get()) < 18 ? "sensorLandscape" : "userLandscape"); 1748 1749 setAttributeIfNotPresent (*act, "android:screenOrientation", landscapeString); 1750 } 1751 else 1752 { 1753 setAttributeIfNotPresent (*act, "android:screenOrientation", androidScreenOrientation.get()); 1754 } 1755 1756 setAttributeIfNotPresent (*act, "android:launchMode", "singleTask"); 1757 1758 // Using the 2D acceleration slows down OpenGL. We *do* enable it here for the activity though, and we disable it 1759 // in each ComponentPeerView instead. This way any embedded native views, which are not children of ComponentPeerView, 1760 // can still use hardware acceleration if needed (e.g. web view). 1761 if (static_cast<int> (androidMinimumSDK.get()) >= 11) 1762 { 1763 if (! act->hasAttribute ("android:hardwareAccelerated")) 1764 act->setAttribute ("android:hardwareAccelerated", "true"); // (using the 2D acceleration slows down openGL) 1765 } 1766 else 1767 { 1768 act->removeAttribute ("android:hardwareAccelerated"); 1769 } 1770 1771 return act; 1772 } 1773 createIntentElement(XmlElement & application)1774 void createIntentElement (XmlElement& application) const 1775 { 1776 auto* intent = getOrCreateChildWithName (application, "intent-filter"); 1777 1778 auto* action = getOrCreateChildWithName (*intent, "action"); 1779 setAttributeIfNotPresent (*action, "android:name", "android.intent.action.MAIN"); 1780 1781 auto* category = getOrCreateChildWithName (*intent, "category"); 1782 setAttributeIfNotPresent (*category, "android:name", "android.intent.category.LAUNCHER"); 1783 } 1784 createServiceElements(XmlElement & application)1785 void createServiceElements (XmlElement& application) const 1786 { 1787 if (areRemoteNotificationsEnabled()) 1788 { 1789 auto* service = application.createNewChildElement ("service"); 1790 service->setAttribute ("android:name", "com.rmsl.juce.JuceFirebaseMessagingService"); 1791 auto* intentFilter = service->createNewChildElement ("intent-filter"); 1792 intentFilter->createNewChildElement ("action")->setAttribute ("android:name", "com.google.firebase.MESSAGING_EVENT"); 1793 1794 service = application.createNewChildElement ("service"); 1795 service->setAttribute ("android:name", "com.rmsl.juce.JuceFirebaseInstanceIdService"); 1796 intentFilter = service->createNewChildElement ("intent-filter"); 1797 intentFilter->createNewChildElement ("action")->setAttribute ("android:name", "com.google.firebase.INSTANCE_ID_EVENT"); 1798 1799 auto* metaData = application.createNewChildElement ("meta-data"); 1800 metaData->setAttribute ("android:name", "firebase_analytics_collection_deactivated"); 1801 metaData->setAttribute ("android:value", "true"); 1802 } 1803 } 1804 createProviderElement(XmlElement & application)1805 void createProviderElement (XmlElement& application) const 1806 { 1807 if (isContentSharingEnabled()) 1808 { 1809 auto* provider = application.createNewChildElement ("provider"); 1810 1811 provider->setAttribute ("android:name", "com.rmsl.juce.JuceSharingContentProvider"); 1812 provider->setAttribute ("android:authorities", project.getBundleIdentifierString().toLowerCase() + ".sharingcontentprovider"); 1813 provider->setAttribute ("android:grantUriPermissions", "true"); 1814 provider->setAttribute ("android:exported", "true"); 1815 } 1816 } 1817 getOrCreateChildWithName(XmlElement & element,const String & childName)1818 static XmlElement* getOrCreateChildWithName (XmlElement& element, const String& childName) 1819 { 1820 auto* child = element.getChildByName (childName); 1821 1822 if (child == nullptr) 1823 child = element.createNewChildElement (childName); 1824 1825 return child; 1826 } 1827 setAttributeIfNotPresent(XmlElement & element,const Identifier & attribute,const String & value)1828 static void setAttributeIfNotPresent (XmlElement& element, const Identifier& attribute, const String& value) 1829 { 1830 if (! element.hasAttribute (attribute.toString())) 1831 element.setAttribute (attribute, value); 1832 } 1833 getPermissionsRequired()1834 StringArray getPermissionsRequired() const 1835 { 1836 StringArray s = StringArray::fromTokens (androidOtherPermissions.get().toString(), ", ", {}); 1837 1838 if (androidInternetNeeded.get()) 1839 { 1840 s.add ("android.permission.INTERNET"); 1841 s.add ("android.permission.CHANGE_WIFI_MULTICAST_STATE"); 1842 } 1843 1844 if (androidMicNeeded.get()) 1845 s.add ("android.permission.RECORD_AUDIO"); 1846 1847 if (androidCameraNeeded.get()) 1848 s.add ("android.permission.CAMERA"); 1849 1850 if (androidBluetoothNeeded.get()) 1851 { 1852 s.add ("android.permission.BLUETOOTH"); 1853 s.add ("android.permission.BLUETOOTH_ADMIN"); 1854 s.add ("android.permission.ACCESS_FINE_LOCATION"); 1855 } 1856 1857 if (androidExternalReadPermission.get()) 1858 s.add ("android.permission.READ_EXTERNAL_STORAGE"); 1859 1860 if (androidExternalWritePermission.get()) 1861 s.add ("android.permission.WRITE_EXTERNAL_STORAGE"); 1862 1863 if (isInAppBillingEnabled()) 1864 s.add ("com.android.vending.BILLING"); 1865 1866 if (androidVibratePermission.get()) 1867 s.add ("android.permission.VIBRATE"); 1868 1869 return getCleanedStringArray (s); 1870 } 1871 1872 //============================================================================== isLibrary()1873 bool isLibrary() const 1874 { 1875 return getProject().getProjectType().isDynamicLibrary() 1876 || getProject().getProjectType().isStaticLibrary(); 1877 } 1878 toGradleList(const StringArray & array)1879 static String toGradleList (const StringArray& array) 1880 { 1881 StringArray escapedArray; 1882 1883 for (auto element : array) 1884 escapedArray.add ("\"" + element.replace ("\\", "\\\\").replace ("\"", "\\\"") + "\""); 1885 1886 return escapedArray.joinIntoString (", "); 1887 } 1888 supportsGLv3()1889 bool supportsGLv3() const 1890 { 1891 return (static_cast<int> (androidMinimumSDK.get()) >= 18); 1892 } 1893 1894 //============================================================================== 1895 const File AndroidExecutable; 1896 1897 JUCE_DECLARE_NON_COPYABLE (AndroidProjectExporter) 1898 }; 1899