/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2015 - ROLI Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ class AndroidStudioProjectExporter : public AndroidProjectExporterBase { public: //============================================================================== bool usesMMFiles() const override { return false; } bool canCopeWithDuplicateFiles() override { return false; } bool supportsUserDefinedConfigurations() const override { return false; } bool isAndroidStudio() const override { return true; } bool isAndroidAnt() const override { return false; } static const char* getName() { return "Android Studio"; } static const char* getValueTreeTypeName() { return "ANDROIDSTUDIO"; } static AndroidStudioProjectExporter* createForSettings (Project& project, const ValueTree& settings) { if (settings.hasType (getValueTreeTypeName())) return new AndroidStudioProjectExporter (project, settings); return nullptr; } //============================================================================== CachedValue gradleVersion, gradleWrapperVersion, gradleToolchain, buildToolsVersion; //============================================================================== AndroidStudioProjectExporter (Project& p, const ValueTree& t) : AndroidProjectExporterBase (p, t), gradleVersion (settings, Ids::gradleVersion, nullptr, "2.10"), gradleWrapperVersion (settings, Ids::gradleWrapperVersion, nullptr, "0.7.0-rc1"), gradleToolchain (settings, Ids::gradleToolchain, nullptr, "clang"), buildToolsVersion (settings, Ids::buildToolsVersion, nullptr, "23.0.2"), androidStudioExecutable (findAndroidStudioExecutable()) { name = getName(); if (getTargetLocationString().isEmpty()) getTargetLocationValue() = getDefaultBuildsRootFolder() + "AndroidStudio"; } //============================================================================== void createToolchainExporterProperties (PropertyListBuilder& props) override { props.add (new TextWithDefaultPropertyComponent (gradleVersion, "gradle version", 32), "The version of gradle that Android Studio should use to build this app"); props.add (new TextWithDefaultPropertyComponent (gradleWrapperVersion, "gradle-experimental wrapper version", 32), "The version of the gradle-experimental wrapper that Android Studio should use to build this app"); static const char* toolchains[] = { "clang", "gcc", nullptr }; props.add (new ChoicePropertyComponent (gradleToolchain.getPropertyAsValue(), "NDK Toolchain", StringArray (toolchains), Array (toolchains)), "The toolchain that gradle should invoke for NDK compilation (variable model.android.ndk.tooclhain in app/build.gradle)"); props.add (new TextWithDefaultPropertyComponent (buildToolsVersion, "Android build tools version", 32), "The Android build tools version that Android Studio should use to build this app"); } void createLibraryModuleExporterProperties (PropertyListBuilder&) override { // gradle cannot do native library modules as far as we know... } //============================================================================== bool canLaunchProject() override { return androidStudioExecutable.exists(); } bool launchProject() override { if (! androidStudioExecutable.exists()) { jassertfalse; return false; } const File targetFolder (getTargetFolder()); // we have to surround the path with extra quotes, otherwise Android Studio // will choke if there are any space characters in the path. return androidStudioExecutable.startAsProcess ("\"" + targetFolder.getFullPathName() + "\""); } //============================================================================== void create (const OwnedArray& modules) const override { const File targetFolder (getTargetFolder()); removeOldFiles (targetFolder); { const String package (getActivityClassPackage()); const String path (package.replaceCharacter ('.', File::separator)); const File javaTarget (targetFolder.getChildFile ("app/src/main/java").getChildFile (path)); copyActivityJavaFiles (modules, javaTarget, package); } writeFile (targetFolder, "settings.gradle", getSettingsGradleFileContent()); writeFile (targetFolder, "build.gradle", getProjectBuildGradleFileContent()); writeFile (targetFolder, "app/build.gradle", getAppBuildGradleFileContent()); writeFile (targetFolder, "local.properties", getLocalPropertiesFileContent()); writeFile (targetFolder, "gradle/wrapper/gradle-wrapper.properties", getGradleWrapperPropertiesFileContent()); writeAndroidManifest (targetFolder); writeStringsXML (targetFolder); writeAppIcons (targetFolder); createSourceSymlinks (targetFolder); } void removeOldFiles (const File& targetFolder) const { targetFolder.getChildFile ("app/src").deleteRecursively(); targetFolder.getChildFile ("app/build").deleteRecursively(); targetFolder.getChildFile ("app/build.gradle").deleteFile(); targetFolder.getChildFile ("gradle").deleteRecursively(); targetFolder.getChildFile ("local.properties").deleteFile(); targetFolder.getChildFile ("settings.gradle").deleteFile(); } void writeFile (const File& gradleProjectFolder, const String& filePath, const String& fileContent) const { MemoryOutputStream outStream; outStream << fileContent; overwriteFileIfDifferentOrThrow (gradleProjectFolder.getChildFile (filePath), outStream); } //============================================================================== static File findAndroidStudioExecutable() { #if JUCE_WINDOWS const File defaultInstallation ("C:\\Program Files\\Android\\Android Studio\\bin"); if (defaultInstallation.exists()) { { const File studio64 = defaultInstallation.getChildFile ("studio64.exe"); if (studio64.existsAsFile()) return studio64; } { const File studio = defaultInstallation.getChildFile ("studio.exe"); if (studio.existsAsFile()) return studio; } } #elif JUCE_MAC const File defaultInstallation ("/Applications/Android Studio.app"); if (defaultInstallation.exists()) return defaultInstallation; #endif return File::nonexistent; } protected: //============================================================================== class AndroidStudioBuildConfiguration : public BuildConfiguration { public: AndroidStudioBuildConfiguration (Project& p, const ValueTree& settings, const ProjectExporter& e) : BuildConfiguration (p, settings, e) { if (getArchitectures().isEmpty()) { if (isDebug()) getArchitecturesValue() = "armeabi x86"; else getArchitecturesValue() = "armeabi armeabi-v7a x86"; } } Value getArchitecturesValue() { return getValue (Ids::androidArchitectures); } String getArchitectures() const { return config [Ids::androidArchitectures]; } var getDefaultOptimisationLevel() const override { return var ((int) (isDebug() ? gccO0 : gccO3)); } void createConfigProperties (PropertyListBuilder& props) override { addGCCOptimisationProperty (props); props.add (new TextPropertyComponent (getArchitecturesValue(), "Architectures", 256, false), "A list of the ARM architectures to build (for a fat binary)."); } }; BuildConfiguration::Ptr createBuildConfig (const ValueTree& v) const override { return new AndroidStudioBuildConfiguration (project, v, *this); } private: static void createSymlinkAndParentFolders (const File& originalFile, const File& linkFile) { { const File linkFileParentDirectory (linkFile.getParentDirectory()); // this will recursively creative the parent directories for the file. // without this, the symlink would fail because it doesn't automatically create // the folders if they don't exist if (! linkFileParentDirectory.createDirectory()) throw SaveError (String ("Could not create directory ") + linkFileParentDirectory.getFullPathName()); } if (! originalFile.createSymbolicLink (linkFile, true)) throw SaveError (String ("Failed to create symlink from ") + linkFile.getFullPathName() + " to " + originalFile.getFullPathName() + "!"); } void makeSymlinksForGroup (const Project::Item& group, const File& targetFolder) const { if (! group.isGroup()) { throw SaveError ("makeSymlinksForGroup was called with something other than a group!"); } for (int i = 0; i < group.getNumChildren(); ++i) { const Project::Item& projectItem = group.getChild (i); if (projectItem.isGroup()) { makeSymlinksForGroup (projectItem, targetFolder.getChildFile (projectItem.getName())); } else if (projectItem.shouldBeAddedToTargetProject()) // must be a file then { const File originalFile (projectItem.getFile()); const File targetFile (targetFolder.getChildFile (originalFile.getFileName())); createSymlinkAndParentFolders (originalFile, targetFile); } } } void createSourceSymlinks (const File& folder) const { const File targetFolder (folder.getChildFile ("app/src/main/jni")); // here we make symlinks to only to files included in the groups inside the project // this is because Android Studio does not have a concept of groups and just uses // the file system layout to determine what's to be compiled { const Array& groups = getAllGroups(); for (int i = 0; i < groups.size(); ++i) { const Project::Item projectItem (groups.getReference (i)); const String projectItemName (projectItem.getName()); if (projectItem.isGroup()) makeSymlinksForGroup (projectItem, projectItemName == "Juce Modules" ? targetFolder.getChildFile ("JuceModules") : targetFolder); } } } void writeAppIcons (const File& folder) const { writeIcons (folder.getChildFile ("app/src/main/res/")); } static String sanitisePath (String path) { return expandHomeFolderToken (path).replace ("\\", "\\\\"); } static String expandHomeFolderToken (const String& path) { String homeFolder = File::getSpecialLocation (File::userHomeDirectory).getFullPathName(); return path.replace ("${user.home}", homeFolder) .replace ("~", homeFolder); } //============================================================================== struct GradleElement { virtual ~GradleElement() {} String toString() const { return toStringIndented (0); } virtual String toStringIndented (int indentLevel) const = 0; static String indent (int indentLevel) { return String::repeatedString (" ", indentLevel); } }; //============================================================================== struct GradleStatement : public GradleElement { GradleStatement (const String& s) : statement (s) {} String toStringIndented (int indentLevel) const override { return indent (indentLevel) + statement; } String statement; }; //============================================================================== struct GradleCppFlag : public GradleStatement { GradleCppFlag (const String& flag) : GradleStatement ("cppFlags.add(" + flag.quoted() + ")") {} }; struct GradlePreprocessorDefine : public GradleStatement { GradlePreprocessorDefine (const String& define, const String& value) : GradleStatement ("cppFlags.add(\"-D" + define + "=" + value + "\")") {} }; struct GradleHeaderIncludePath : public GradleStatement { GradleHeaderIncludePath (const String& path) : GradleStatement ("cppFlags.add(\"-I${project.rootDir}/" + sanitisePath (path) + "\".toString())") {} }; struct GradleLibrarySearchPath : public GradleStatement { GradleLibrarySearchPath (const String& path) : GradleStatement ("cppFlags.add(\"-L" + sanitisePath (path) + "\".toString())") {} }; struct GradleLinkerFlag : public GradleStatement { GradleLinkerFlag (const String& flag) : GradleStatement ("ldFlags.add(" + flag.quoted() + "\")") {} }; struct GradleLinkLibrary : public GradleStatement { GradleLinkLibrary (const String& lib) : GradleStatement ("ldLibs.add(" + lib.quoted() + ")") {} }; //============================================================================== struct GradleValue : public GradleElement { template GradleValue (const String& k, const ValueType& v) : key (k), value (v) {} GradleValue (const String& k, bool boolValue) : key (k), value (boolValue ? "true" : "false") {} String toStringIndented (int indentLevel) const override { return indent (indentLevel) + key + " = " + value; } protected: String key, value; }; struct GradleString : public GradleValue { GradleString (const String& k, const String& str) : GradleValue (k, str.quoted()) { if (str.containsAnyOf ("${\"\'")) value += ".toString()"; } }; struct GradleFilePath : public GradleValue { GradleFilePath (const String& k, const String& path) : GradleValue (k, "new File(\"" + sanitisePath (path) + "\")") {} }; //============================================================================== struct GradleObject : public GradleElement { GradleObject (const String& nm) : name (nm) {} #if JUCE_COMPILER_SUPPORTS_VARIADIC_TEMPLATES template void add (Args... args) { children.add (new GradleType (args...)); // Note: can't use std::forward because it doesn't compile for OS X 10.8 } #else // Remove this workaround once we drop VS2012 support! template void add (Arg1 arg1) { children.add (new GradleType (arg1)); } template void add (Arg1 arg1, Arg2 arg2) { children.add (new GradleType (arg1, arg2)); } #endif void addChildObject (GradleObject* objectToAdd) noexcept { children.add (objectToAdd); } String toStringIndented (int indentLevel) const override { String result; result << indent (indentLevel) << name << " {" << newLine; for (const auto& child : children) result << child->toStringIndented (indentLevel + 1) << newLine; result << indent (indentLevel) << "}"; if (indentLevel == 0) result << newLine; return result; } private: String name; OwnedArray children; }; //============================================================================== String getSettingsGradleFileContent() const { return "include ':app'"; } String getProjectBuildGradleFileContent() const { String projectBuildGradle; projectBuildGradle << getGradleBuildScript(); projectBuildGradle << getGradleAllProjects(); return projectBuildGradle; } //============================================================================== String getGradleBuildScript() const { GradleObject buildScript ("buildscript"); buildScript.addChildObject (getGradleRepositories()); buildScript.addChildObject (getGradleDependencies()); return buildScript.toString(); } GradleObject* getGradleRepositories() const { auto repositories = new GradleObject ("repositories"); repositories->add ("jcenter()"); return repositories; } GradleObject* getGradleDependencies() const { auto dependencies = new GradleObject ("dependencies"); dependencies->add ("classpath 'com.android.tools.build:gradle-experimental:" + gradleWrapperVersion.get() + "'"); return dependencies; } String getGradleAllProjects() const { GradleObject allProjects ("allprojects"); allProjects.addChildObject (getGradleRepositories()); return allProjects.toString(); } //============================================================================== String getAppBuildGradleFileContent() const { String appBuildGradle; appBuildGradle << "apply plugin: 'com.android.model.application'" << newLine; appBuildGradle << getAndroidModel(); appBuildGradle << getAppDependencies(); return appBuildGradle; } String getAndroidModel() const { GradleObject model ("model"); model.addChildObject (getAndroidObject()); model.addChildObject (getAndroidNdkSettings()); model.addChildObject (getAndroidSources()); model.addChildObject (getAndroidBuildConfigs()); model.addChildObject (getAndroidSigningConfigs()); model.addChildObject (getAndroidProductFlavours()); return model.toString(); } String getAppDependencies() const { GradleObject dependencies ("dependencies"); dependencies.add ("compile \"com.android.support:support-v4:+\""); return dependencies.toString(); } //============================================================================== GradleObject* getAndroidObject() const { auto android = new GradleObject ("android"); android->add ("compileSdkVersion", androidMinimumSDK.get().getIntValue()); android->add ("buildToolsVersion", buildToolsVersion.get()); android->addChildObject (getAndroidDefaultConfig()); return android; } GradleObject* getAndroidDefaultConfig() const { const String bundleIdentifier = project.getBundleIdentifier().toString().toLowerCase(); const int minSdkVersion = androidMinimumSDK.get().getIntValue(); auto defaultConfig = new GradleObject ("defaultConfig.with"); defaultConfig->add ("applicationId", bundleIdentifier); defaultConfig->add ("minSdkVersion.apiLevel", minSdkVersion); defaultConfig->add ("targetSdkVersion.apiLevel", minSdkVersion); return defaultConfig; } GradleObject* getAndroidNdkSettings() const { const String toolchain = gradleToolchain.get(); const bool isClang = (toolchain == "clang"); auto ndkSettings = new GradleObject ("android.ndk"); ndkSettings->add ("moduleName", "juce_jni"); ndkSettings->add ("toolchain", toolchain); ndkSettings->add ("stl", isClang ? "c++_static" : "gnustl_static"); addAllNdkCompilerSettings (ndkSettings); return ndkSettings; } void addAllNdkCompilerSettings (GradleObject* ndk) const { addNdkCppFlags (ndk); addNdkPreprocessorDefines (ndk); addNdkHeaderIncludePaths (ndk); addNdkLinkerFlags (ndk); addNdkLibraries (ndk); } void addNdkCppFlags (GradleObject* ndk) const { const char* alwaysUsedFlags[] = { "-fsigned-char", "-fexceptions", "-frtti", "-std=c++11", nullptr }; StringArray cppFlags (alwaysUsedFlags); cppFlags.mergeArray (StringArray::fromTokens (getExtraCompilerFlagsString(), " ", "")); for (int i = 0; i < cppFlags.size(); ++i) ndk->add (cppFlags[i]); } void addNdkPreprocessorDefines (GradleObject* ndk) const { const auto& defines = getAllPreprocessorDefs(); for (int i = 0; i < defines.size(); ++i) ndk->add ( defines.getAllKeys()[i], defines.getAllValues()[i]); } void addNdkHeaderIncludePaths (GradleObject* ndk) const { StringArray includePaths; for (const auto& cppFile : getAllCppFilesToBeIncludedWithPath()) includePaths.addIfNotAlreadyThere (cppFile.getParentDirectory().toUnixStyle()); for (const auto& path : includePaths) ndk->add (path); } Array getAllCppFilesToBeIncludedWithPath() const { Array cppFiles; struct NeedsToBeIncludedWithPathPredicate { bool operator() (const Project::Item& projectItem) const { return projectItem.shouldBeAddedToTargetProject() && ! projectItem.isModuleCode(); } }; for (const auto& group : getAllGroups()) findAllProjectItemsWithPredicate (group, cppFiles, NeedsToBeIncludedWithPathPredicate()); return cppFiles; } void addNdkLinkerFlags (GradleObject* ndk) const { const auto linkerFlags = StringArray::fromTokens (getExtraLinkerFlagsString(), " ", ""); for (const auto& flag : linkerFlags) ndk->add (flag); } void addNdkLibraries (GradleObject* ndk) const { const char* requiredAndroidLibs[] = { "android", "EGL", "GLESv2", "log", nullptr }; StringArray libs (requiredAndroidLibs); libs.addArray (StringArray::fromTokens(getExternalLibrariesString(), ";", "")); for (const auto& lib : libs) ndk->add (lib); } GradleObject* getAndroidSources() const { auto source = new GradleObject ("source"); // app source folder source->add ("exclude \"**/JuceModules/\""); auto jni = new GradleObject ("jni"); // all C++ sources for app jni->addChildObject (source); auto main = new GradleObject ("main"); // all sources for app main->addChildObject (jni); auto sources = new GradleObject ("android.sources"); // all sources sources->addChildObject (main); return sources; } GradleObject* getAndroidBuildConfigs() const { auto buildConfigs = new GradleObject ("android.buildTypes"); for (ConstConfigIterator config (*this); config.next();) buildConfigs->addChildObject (getBuildConfig (*config)); return buildConfigs; } GradleObject* getBuildConfig (const BuildConfiguration& config) const { const String configName (config.getName()); // Note: at the moment, Android Studio only supports a "debug" and a "release" // build config, but no custom build configs like Projucer's other exporters do. if (configName != "Debug" && configName != "Release") throw SaveError ("Build configurations other than Debug and Release are not yet support for Android Studio"); auto gradleConfig = new GradleObject (configName.toLowerCase()); if (! config.isDebug()) gradleConfig->add ("signingConfig", "$(\"android.signingConfigs.releaseConfig\")"); addConfigNdkSettings (gradleConfig, config); return gradleConfig; } void addConfigNdkSettings (GradleObject* buildConfig, const BuildConfiguration& config) const { auto ndkSettings = new GradleObject ("ndk.with"); if (config.isDebug()) { ndkSettings->add ("debuggable", true); ndkSettings->add ("-g"); ndkSettings->add ("DEBUG", "1"); ndkSettings->add ("_DEBUG", "1"); } else { ndkSettings->add ("NDEBUG", "1"); } ndkSettings->add ("-O" + config.getGCCOptimisationFlag()); for (const auto& path : getHeaderSearchPaths (config)) ndkSettings->add (path); for (const auto& path : config.getLibrarySearchPaths()) ndkSettings->add (path); ndkSettings->add ("JUCE_ANDROID", "1"); ndkSettings->add ("JUCE_ANDROID_API_VERSION", androidMinimumSDK.get()); ndkSettings->add ("JUCE_ANDROID_ACTIVITY_CLASSNAME", getJNIActivityClassName().replaceCharacter ('/', '_')); ndkSettings->add ("JUCE_ANDROID_ACTIVITY_CLASSPATH","\\\"" + androidActivityClass.get().replaceCharacter('.', '/') + "\\\""); const auto defines = config.getAllPreprocessorDefs(); for (int i = 0; i < defines.size(); ++i) ndkSettings->add (defines.getAllKeys()[i], defines.getAllValues()[i]); buildConfig->addChildObject (ndkSettings); } StringArray getHeaderSearchPaths (const BuildConfiguration& config) const { StringArray paths (extraSearchPaths); paths.addArray (config.getHeaderSearchPaths()); paths = getCleanedStringArray (paths); return paths; } GradleObject* getAndroidSigningConfigs() const { auto releaseConfig = new GradleObject ("create(\"releaseConfig\")"); releaseConfig->add ("storeFile", androidKeyStore.get()); releaseConfig->add ("storePassword", androidKeyStorePass.get()); releaseConfig->add ("keyAlias", androidKeyAlias.get()); releaseConfig->add ("keyPassword", androidKeyAliasPass.get()); releaseConfig->add ("storeType", "jks"); auto signingConfigs = new GradleObject ("android.signingConfigs"); signingConfigs->addChildObject (releaseConfig); // Note: no need to add a debugConfig, Android Studio will use debug.keystore by default return signingConfigs; } GradleObject* getAndroidProductFlavours() const { auto flavours = new GradleObject ("android.productFlavors"); StringArray architectures (StringArray::fromTokens (getABIs (true), " ", "")); architectures.mergeArray (StringArray::fromTokens (getABIs (false), " ", "")); if (architectures.size() == 0) throw SaveError ("Can't build for no architectures!"); for (int i = 0; i < architectures.size(); ++i) { String arch (architectures[i].trim()); if ((arch).isEmpty()) continue; flavours->addChildObject (getGradleProductFlavourForArch (arch)); } return flavours; } GradleObject* getGradleProductFlavourForArch (const String& arch) const { auto flavour = new GradleObject ("create(\"" + arch + "\")"); flavour->add ("ndk.abiFilters.add(\"" + arch + "\")"); return flavour; } //============================================================================== String getLocalPropertiesFileContent() const { String props; props << "ndk.dir=" << sanitisePath (ndkPath.toString()) << newLine << "sdk.dir=" << sanitisePath (sdkPath.toString()) << newLine; return props; } String getGradleWrapperPropertiesFileContent() const { String props; props << "distributionUrl=https\\://services.gradle.org/distributions/gradle-" << gradleVersion.get() << "-all.zip"; return props; } //============================================================================== void writeStringsXML (const File& folder) const { XmlElement strings ("resources"); XmlElement* resourceName = strings.createNewChildElement ("string"); resourceName->setAttribute ("name", "app_name"); resourceName->addTextElement (projectName); writeXmlOrThrow (strings, folder.getChildFile ("app/src/main/res/values/string.xml"), "utf-8", 100, true); } //============================================================================== void writeAndroidManifest (const File& folder) const { ScopedPointer manifest (createManifestXML()); writeXmlOrThrow (*manifest, folder.getChildFile ("app/src/main/AndroidManifest.xml"), "utf-8", 100, true); } //============================================================================== const File androidStudioExecutable; JUCE_DECLARE_NON_COPYABLE (AndroidStudioProjectExporter) };