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