The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

483 lines
22KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2015 - ROLI Ltd.
  5. Permission is granted to use this software under the terms of either:
  6. a) the GPL v2 (or any later version)
  7. b) the Affero GPL v3
  8. Details of these licenses can be found at: www.gnu.org/licenses
  9. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  10. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  11. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  12. ------------------------------------------------------------------------------
  13. To release a closed-source product which uses JUCE, commercial licenses are
  14. available: visit www.juce.com for more information.
  15. ==============================================================================
  16. */
  17. class AndroidProjectExporterBase : public ProjectExporter
  18. {
  19. public:
  20. //==============================================================================
  21. AndroidProjectExporterBase (Project& p, const ValueTree& t)
  22. : ProjectExporter (p, t),
  23. androidScreenOrientation (settings, Ids::androidScreenOrientation, nullptr, "unspecified"),
  24. androidActivityClass (settings, Ids::androidActivityClass, nullptr, createDefaultClassName()),
  25. androidActivitySubClassName (settings, Ids::androidActivitySubClassName, nullptr),
  26. androidVersionCode (settings, Ids::androidVersionCode, nullptr, "1"),
  27. androidMinimumSDK (settings, Ids::androidMinimumSDK, nullptr, "23"),
  28. androidTheme (settings, Ids::androidTheme, nullptr),
  29. androidInternetNeeded (settings, Ids::androidInternetNeeded, nullptr, true),
  30. androidMicNeeded (settings, Ids::microphonePermissionNeeded, nullptr, false),
  31. androidBluetoothNeeded (settings, Ids::androidBluetoothNeeded, nullptr, true),
  32. androidOtherPermissions (settings, Ids::androidOtherPermissions, nullptr),
  33. androidKeyStore (settings, Ids::androidKeyStore, nullptr, "${user.home}/.android/debug.keystore"),
  34. androidKeyStorePass (settings, Ids::androidKeyStorePass, nullptr, "android"),
  35. androidKeyAlias (settings, Ids::androidKeyAlias, nullptr, "androiddebugkey"),
  36. androidKeyAliasPass (settings, Ids::androidKeyAliasPass, nullptr, "android")
  37. {
  38. initialiseDependencyPathValues();
  39. }
  40. //==============================================================================
  41. bool isXcode() const override { return false; }
  42. bool isVisualStudio() const override { return false; }
  43. bool isCodeBlocks() const override { return false; }
  44. bool isMakefile() const override { return false; }
  45. bool isAndroid() const override { return true; }
  46. bool isWindows() const override { return false; }
  47. bool isLinux() const override { return false; }
  48. bool isOSX() const override { return false; }
  49. bool isiOS() const override { return false; }
  50. bool supportsTargetType (ProjectType::Target::Type type) const override
  51. {
  52. switch (type)
  53. {
  54. case ProjectType::Target::GUIApp:
  55. case ProjectType::Target::StaticLibrary:
  56. return true;
  57. default:
  58. break;
  59. }
  60. return false;
  61. }
  62. //==============================================================================
  63. void create (const OwnedArray<LibraryModule>& modules) const override
  64. {
  65. const String package (getActivityClassPackage());
  66. const String path (package.replaceCharacter ('.', File::separator));
  67. const File target (getTargetFolder().getChildFile ("src").getChildFile (path));
  68. copyActivityJavaFiles (modules, target, package);
  69. }
  70. //==============================================================================
  71. void addPlatformSpecificSettingsForProjectType (const ProjectType&) override
  72. {
  73. // no-op.
  74. }
  75. //==============================================================================
  76. void createExporterProperties (PropertyListBuilder& props) override
  77. {
  78. createBaseExporterProperties (props);
  79. createToolchainExporterProperties (props);
  80. createManifestExporterProperties (props);
  81. createLibraryModuleExporterProperties (props);
  82. createCodeSigningExporterProperties (props);
  83. createOtherExporterProperties (props);
  84. }
  85. //==============================================================================
  86. enum ScreenOrientation
  87. {
  88. unspecified = 1,
  89. portrait = 2,
  90. landscape = 3
  91. };
  92. //==============================================================================
  93. CachedValue<String> androidScreenOrientation, androidActivityClass, androidActivitySubClassName,
  94. androidVersionCode, androidMinimumSDK, androidTheme;
  95. CachedValue<bool> androidInternetNeeded, androidMicNeeded, androidBluetoothNeeded;
  96. CachedValue<String> androidOtherPermissions;
  97. CachedValue<String> androidKeyStore, androidKeyStorePass, androidKeyAlias, androidKeyAliasPass;
  98. //==============================================================================
  99. void createBaseExporterProperties (PropertyListBuilder& props)
  100. {
  101. static const char* orientations[] = { "Portrait and Landscape", "Portrait", "Landscape", nullptr };
  102. static const char* orientationValues[] = { "unspecified", "portrait", "landscape", nullptr };
  103. props.add (new ChoicePropertyComponent (androidScreenOrientation.getPropertyAsValue(), "Screen orientation", StringArray (orientations), Array<var> (orientationValues)),
  104. "The screen orientations that this app should support");
  105. props.add (new TextWithDefaultPropertyComponent<String> (androidActivityClass, "Android Activity class name", 256),
  106. "The full java class name to use for the app's Activity class.");
  107. props.add (new TextPropertyComponent (androidActivitySubClassName.getPropertyAsValue(), "Android Activity sub-class name", 256, false),
  108. "If not empty, specifies the Android Activity class name stored in the app's manifest. "
  109. "Use this if you would like to use your own Android Activity sub-class.");
  110. props.add (new TextWithDefaultPropertyComponent<String> (androidVersionCode, "Android Version Code", 32),
  111. "An integer value that represents the version of the application code, relative to other versions.");
  112. props.add (new DependencyPathPropertyComponent (project.getFile().getParentDirectory(), sdkPath, "Android SDK Path"),
  113. "The path to the Android SDK folder on the target build machine");
  114. props.add (new DependencyPathPropertyComponent (project.getFile().getParentDirectory(), ndkPath, "Android NDK Path"),
  115. "The path to the Android NDK folder on the target build machine");
  116. props.add (new TextWithDefaultPropertyComponent<String> (androidMinimumSDK, "Minimum SDK version", 32),
  117. "The number of the minimum version of the Android SDK that the app requires");
  118. }
  119. //==============================================================================
  120. virtual void createToolchainExporterProperties (PropertyListBuilder& props) = 0; // different for ant and Android Studio
  121. //==============================================================================
  122. void createManifestExporterProperties (PropertyListBuilder& props)
  123. {
  124. props.add (new BooleanPropertyComponent (androidInternetNeeded.getPropertyAsValue(), "Internet Access", "Specify internet access permission in the manifest"),
  125. "If enabled, this will set the android.permission.INTERNET flag in the manifest.");
  126. props.add (new BooleanPropertyComponent (androidMicNeeded.getPropertyAsValue(), "Audio Input Required", "Specify audio record permission in the manifest"),
  127. "If enabled, this will set the android.permission.RECORD_AUDIO flag in the manifest.");
  128. props.add (new BooleanPropertyComponent (androidBluetoothNeeded.getPropertyAsValue(), "Bluetooth permissions Required", "Specify bluetooth permission (required for Bluetooth MIDI)"),
  129. "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.");
  130. props.add (new TextPropertyComponent (androidOtherPermissions.getPropertyAsValue(), "Custom permissions", 2048, false),
  131. "A space-separated list of other permission flags that should be added to the manifest.");
  132. }
  133. //==============================================================================
  134. virtual void createLibraryModuleExporterProperties (PropertyListBuilder& props) = 0; // different for ant and Android Studio
  135. //==============================================================================
  136. void createCodeSigningExporterProperties (PropertyListBuilder& props)
  137. {
  138. props.add (new TextWithDefaultPropertyComponent<String> (androidKeyStore, "Key Signing: key.store", 2048),
  139. "The key.store value, used when signing the package.");
  140. props.add (new TextWithDefaultPropertyComponent<String> (androidKeyStorePass, "Key Signing: key.store.password", 2048),
  141. "The key.store password, used when signing the package.");
  142. props.add (new TextWithDefaultPropertyComponent<String> (androidKeyAlias, "Key Signing: key.alias", 2048),
  143. "The key.alias value, used when signing the package.");
  144. props.add (new TextWithDefaultPropertyComponent<String> (androidKeyAliasPass, "Key Signing: key.alias.password", 2048),
  145. "The key.alias password, used when signing the package.");
  146. }
  147. //==============================================================================
  148. void createOtherExporterProperties (PropertyListBuilder& props)
  149. {
  150. props.add (new TextPropertyComponent (androidTheme.getPropertyAsValue(), "Android Theme", 256, false),
  151. "E.g. @android:style/Theme.NoTitleBar or leave blank for default");
  152. }
  153. //==============================================================================
  154. String createDefaultClassName() const
  155. {
  156. String s (project.getBundleIdentifier().toString().toLowerCase());
  157. if (s.length() > 5
  158. && s.containsChar ('.')
  159. && s.containsOnly ("abcdefghijklmnopqrstuvwxyz_.")
  160. && ! s.startsWithChar ('.'))
  161. {
  162. if (! s.endsWithChar ('.'))
  163. s << ".";
  164. }
  165. else
  166. {
  167. s = "com.yourcompany.";
  168. }
  169. return s + CodeHelpers::makeValidIdentifier (project.getProjectFilenameRoot(), false, true, false);
  170. }
  171. void initialiseDependencyPathValues()
  172. {
  173. sdkPath.referTo (Value (new DependencyPathValueSource (getSetting (Ids::androidSDKPath),
  174. Ids::androidSDKPath, TargetOS::getThisOS())));
  175. ndkPath.referTo (Value (new DependencyPathValueSource (getSetting (Ids::androidNDKPath),
  176. Ids::androidNDKPath, TargetOS::getThisOS())));
  177. }
  178. void copyActivityJavaFiles (const OwnedArray<LibraryModule>& modules, const File& targetFolder, const String& package) const
  179. {
  180. const String className (getActivityName());
  181. if (className.isEmpty())
  182. throw SaveError ("Invalid Android Activity class name: " + androidActivityClass.get());
  183. createDirectoryOrThrow (targetFolder);
  184. LibraryModule* const coreModule = getCoreModule (modules);
  185. if (coreModule != nullptr)
  186. {
  187. File javaDestFile (targetFolder.getChildFile (className + ".java"));
  188. File javaSourceFolder (coreModule->getFolder().getChildFile ("native")
  189. .getChildFile ("java"));
  190. String juceMidiCode, juceMidiImports, juceRuntimePermissionsCode;
  191. juceMidiImports << newLine;
  192. if (androidMinimumSDK.get().getIntValue() >= 23)
  193. {
  194. File javaAndroidMidi (javaSourceFolder.getChildFile ("AndroidMidi.java"));
  195. File javaRuntimePermissions (javaSourceFolder.getChildFile ("AndroidRuntimePermissions.java"));
  196. juceMidiImports << "import android.media.midi.*;" << newLine
  197. << "import android.bluetooth.*;" << newLine
  198. << "import android.bluetooth.le.*;" << newLine;
  199. juceMidiCode = javaAndroidMidi.loadFileAsString().replace ("JuceAppActivity", className);
  200. juceRuntimePermissionsCode = javaRuntimePermissions.loadFileAsString().replace ("JuceAppActivity", className);
  201. }
  202. else
  203. {
  204. juceMidiCode = javaSourceFolder.getChildFile ("AndroidMidiFallback.java")
  205. .loadFileAsString()
  206. .replace ("JuceAppActivity", className);
  207. }
  208. File javaSourceFile (javaSourceFolder.getChildFile ("JuceAppActivity.java"));
  209. StringArray javaSourceLines (StringArray::fromLines (javaSourceFile.loadFileAsString()));
  210. {
  211. MemoryOutputStream newFile;
  212. for (int i = 0; i < javaSourceLines.size(); ++i)
  213. {
  214. const String& line = javaSourceLines[i];
  215. if (line.contains ("$$JuceAndroidMidiImports$$"))
  216. newFile << juceMidiImports;
  217. else if (line.contains ("$$JuceAndroidMidiCode$$"))
  218. newFile << juceMidiCode;
  219. else if (line.contains ("$$JuceAndroidRuntimePermissionsCode$$"))
  220. newFile << juceRuntimePermissionsCode;
  221. else
  222. newFile << line.replace ("JuceAppActivity", className)
  223. .replace ("package com.juce;", "package " + package + ";") << newLine;
  224. }
  225. javaSourceLines = StringArray::fromLines (newFile.toString());
  226. }
  227. while (javaSourceLines.size() > 2
  228. && javaSourceLines[javaSourceLines.size() - 1].trim().isEmpty()
  229. && javaSourceLines[javaSourceLines.size() - 2].trim().isEmpty())
  230. javaSourceLines.remove (javaSourceLines.size() - 1);
  231. overwriteFileIfDifferentOrThrow (javaDestFile, javaSourceLines.joinIntoString (newLine));
  232. }
  233. }
  234. String getActivityName() const
  235. {
  236. return androidActivityClass.get().fromLastOccurrenceOf (".", false, false);
  237. }
  238. String getActivitySubClassName() const
  239. {
  240. String activityPath = androidActivitySubClassName.get();
  241. return (activityPath.isEmpty()) ? getActivityName() : activityPath.fromLastOccurrenceOf (".", false, false);
  242. }
  243. String getActivityClassPackage() const
  244. {
  245. return androidActivityClass.get().upToLastOccurrenceOf (".", false, false);
  246. }
  247. String getJNIActivityClassName() const
  248. {
  249. return androidActivityClass.get().replaceCharacter ('.', '/');
  250. }
  251. static LibraryModule* getCoreModule (const OwnedArray<LibraryModule>& modules)
  252. {
  253. for (int i = modules.size(); --i >= 0;)
  254. if (modules.getUnchecked(i)->getID() == "juce_core")
  255. return modules.getUnchecked(i);
  256. return nullptr;
  257. }
  258. StringArray getPermissionsRequired() const
  259. {
  260. StringArray s;
  261. s.addTokens (androidOtherPermissions.get(), ", ", "");
  262. if (androidInternetNeeded.get())
  263. s.add ("android.permission.INTERNET");
  264. if (androidMicNeeded.get())
  265. s.add ("android.permission.RECORD_AUDIO");
  266. if (androidBluetoothNeeded.get())
  267. {
  268. s.add ("android.permission.BLUETOOTH");
  269. s.add ("android.permission.BLUETOOTH_ADMIN");
  270. s.add ("android.permission.ACCESS_COARSE_LOCATION");
  271. }
  272. return getCleanedStringArray (s);
  273. }
  274. template <typename PredicateT>
  275. void findAllProjectItemsWithPredicate (const Project::Item& projectItem, Array<RelativePath>& results, const PredicateT& predicate) const
  276. {
  277. if (projectItem.isGroup())
  278. {
  279. for (int i = 0; i < projectItem.getNumChildren(); ++i)
  280. findAllProjectItemsWithPredicate (projectItem.getChild(i), results, predicate);
  281. }
  282. else
  283. {
  284. if (predicate (projectItem))
  285. results.add (RelativePath (projectItem.getFile(), getTargetFolder(), RelativePath::buildTargetFolder));
  286. }
  287. }
  288. void writeIcon (const File& file, const Image& im) const
  289. {
  290. if (im.isValid())
  291. {
  292. createDirectoryOrThrow (file.getParentDirectory());
  293. PNGImageFormat png;
  294. MemoryOutputStream mo;
  295. if (! png.writeImageToStream (im, mo))
  296. throw SaveError ("Can't generate Android icon file");
  297. overwriteFileIfDifferentOrThrow (file, mo);
  298. }
  299. }
  300. void writeIcons (const File& folder) const
  301. {
  302. ScopedPointer<Drawable> bigIcon (getBigIcon());
  303. ScopedPointer<Drawable> smallIcon (getSmallIcon());
  304. if (bigIcon != nullptr && smallIcon != nullptr)
  305. {
  306. const int step = jmax (bigIcon->getWidth(), bigIcon->getHeight()) / 8;
  307. writeIcon (folder.getChildFile ("drawable-xhdpi/icon.png"), getBestIconForSize (step * 8, false));
  308. writeIcon (folder.getChildFile ("drawable-hdpi/icon.png"), getBestIconForSize (step * 6, false));
  309. writeIcon (folder.getChildFile ("drawable-mdpi/icon.png"), getBestIconForSize (step * 4, false));
  310. writeIcon (folder.getChildFile ("drawable-ldpi/icon.png"), getBestIconForSize (step * 3, false));
  311. }
  312. else if (Drawable* icon = bigIcon != nullptr ? bigIcon : smallIcon)
  313. {
  314. writeIcon (folder.getChildFile ("drawable-mdpi/icon.png"), rescaleImageForIcon (*icon, icon->getWidth()));
  315. }
  316. }
  317. template <typename BuildConfigType>
  318. String getABIs (bool forDebug) const
  319. {
  320. for (ConstConfigIterator config (*this); config.next();)
  321. {
  322. const BuildConfigType& androidConfig = dynamic_cast<const BuildConfigType&> (*config);
  323. if (config->isDebug() == forDebug)
  324. return androidConfig.getArchitectures();
  325. }
  326. return String();
  327. }
  328. //==============================================================================
  329. XmlElement* createManifestXML() const
  330. {
  331. XmlElement* manifest = new XmlElement ("manifest");
  332. manifest->setAttribute ("xmlns:android", "http://schemas.android.com/apk/res/android");
  333. manifest->setAttribute ("android:versionCode", androidVersionCode.get());
  334. manifest->setAttribute ("android:versionName", project.getVersionString());
  335. manifest->setAttribute ("package", getActivityClassPackage());
  336. XmlElement* screens = manifest->createNewChildElement ("supports-screens");
  337. screens->setAttribute ("android:smallScreens", "true");
  338. screens->setAttribute ("android:normalScreens", "true");
  339. screens->setAttribute ("android:largeScreens", "true");
  340. //screens->setAttribute ("android:xlargeScreens", "true");
  341. screens->setAttribute ("android:anyDensity", "true");
  342. XmlElement* sdk = manifest->createNewChildElement ("uses-sdk");
  343. sdk->setAttribute ("android:minSdkVersion", androidMinimumSDK.get());
  344. sdk->setAttribute ("android:targetSdkVersion", androidMinimumSDK.get());
  345. {
  346. const StringArray permissions (getPermissionsRequired());
  347. for (int i = permissions.size(); --i >= 0;)
  348. manifest->createNewChildElement ("uses-permission")->setAttribute ("android:name", permissions[i]);
  349. }
  350. if (project.getModules().isModuleEnabled ("juce_opengl"))
  351. {
  352. XmlElement* feature = manifest->createNewChildElement ("uses-feature");
  353. feature->setAttribute ("android:glEsVersion", "0x00020000");
  354. feature->setAttribute ("android:required", "true");
  355. }
  356. XmlElement* app = manifest->createNewChildElement ("application");
  357. app->setAttribute ("android:label", "@string/app_name");
  358. if (androidTheme.get().isNotEmpty())
  359. app->setAttribute ("android:theme", androidTheme.get());
  360. {
  361. ScopedPointer<Drawable> bigIcon (getBigIcon()), smallIcon (getSmallIcon());
  362. if (bigIcon != nullptr || smallIcon != nullptr)
  363. app->setAttribute ("android:icon", "@drawable/icon");
  364. }
  365. if (androidMinimumSDK.get().getIntValue() >= 11)
  366. app->setAttribute ("android:hardwareAccelerated", "false"); // (using the 2D acceleration slows down openGL)
  367. XmlElement* act = app->createNewChildElement ("activity");
  368. act->setAttribute ("android:name", getActivitySubClassName());
  369. act->setAttribute ("android:label", "@string/app_name");
  370. act->setAttribute ("android:configChanges", "keyboardHidden|orientation|screenSize");
  371. act->setAttribute ("android:screenOrientation", androidScreenOrientation.get());
  372. XmlElement* intent = act->createNewChildElement ("intent-filter");
  373. intent->createNewChildElement ("action")->setAttribute ("android:name", "android.intent.action.MAIN");
  374. intent->createNewChildElement ("category")->setAttribute ("android:name", "android.intent.category.LAUNCHER");
  375. return manifest;
  376. }
  377. //==============================================================================
  378. Value sdkPath, ndkPath;
  379. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AndroidProjectExporterBase)
  380. };