From 57ff869db009994a3d7f0a4dab084f5b865db66f Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 19 Mar 2025 14:55:06 +0000 Subject: [PATCH] Projucer: Add manifest option to allow virtual MIDI on Android --- .../jucer_ProjectExport_Android.h | 105 +++++++++++++++++- .../Source/Utility/Helpers/jucer_PresetIDs.h | 1 + 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h index 2bcae85357..a8304ad5c9 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h @@ -34,7 +34,6 @@ #pragma once - //============================================================================== class AndroidProjectExporter final : public ProjectExporter { @@ -113,7 +112,8 @@ public: androidReadMediaVideoPermission, androidExternalWritePermission, androidInAppBillingPermission, androidVibratePermission, androidOtherPermissions, androidPushNotifications, androidEnableRemoteNotifications, androidRemoteNotificationsConfigFile, androidEnableContentSharing, androidKeyStore, - androidKeyStorePass, androidKeyAlias, androidKeyAliasPass, gradleVersion, gradleToolchain, gradleClangTidy, androidPluginVersion; + androidKeyStorePass, androidKeyAlias, androidKeyAliasPass, gradleVersion, gradleToolchain, gradleClangTidy, androidPluginVersion, + androidEnableVirtualMidi; //============================================================================== AndroidProjectExporter (Project& p, const ValueTree& t) @@ -161,6 +161,7 @@ public: gradleToolchain (settings, Ids::gradleToolchain, getUndoManager(), "clang"), gradleClangTidy (settings, Ids::gradleClangTidy, getUndoManager(), false), androidPluginVersion (settings, Ids::androidPluginVersion, getUndoManager(), "8.10.0"), + androidEnableVirtualMidi (settings, Ids::androidEnableVirtualMidi, getUndoManager(), false), AndroidExecutable (getAppSettings().getStoredPath (Ids::androidStudioExePath, TargetOS::getThisOS()).get().toString()) { name = getDisplayName(); @@ -235,6 +236,7 @@ public: { copyAdditionalJavaLibs (appFolder); writeStringsXML (targetFolder); + writeDeviceInfoXML (targetFolder); writeAppIcons (targetFolder); } @@ -999,6 +1001,9 @@ private: if (isInAppBillingEnabled()) addOptJavaFolderToSourceSetsForModule (javaSourceSets, modules, "juce_product_unlocking"); + if (isVirtualMidiEnabled()) + addOptJavaFolderToSourceSetsForModule (javaSourceSets, modules, "juce_audio_devices"); + MemoryOutputStream mo; mo.setNewLineString (getNewLineString()); @@ -1215,6 +1220,11 @@ private: props.add (new TextPropertyComponent (androidRemoteNotificationsConfigFile.getPropertyAsValue(), "Remote Notifications Config File", 2048, false), "Path to google-services.json file. This will be the file provided by Firebase when creating a new app in Firebase console."); + props.add (new ChoicePropertyComponent (androidEnableVirtualMidi, "Enable Virtual MIDI"), + "When enabled, this will add entries to your application manifest declaring that your program " + "can provide the MidiDeviceService and/or MidiUmpDeviceService." + "This has no effect unless the juce_audio_devices module is included in the project."); + props.add (new TextPropertyComponent (androidManifestCustomXmlElements, "Custom Manifest XML Content", 8192, true), "You can specify custom AndroidManifest.xml content overriding the default one generated by Projucer. " "Projucer will automatically create any missing and required XML elements and attributes " @@ -1341,6 +1351,12 @@ private: && androidEnableRemoteNotifications.get(); } + bool isVirtualMidiEnabled() const + { + return project.getEnabledModules().isModuleEnabled ("juce_audio_devices") + && androidEnableVirtualMidi.get(); + } + bool isInAppBillingEnabled() const { return project.getEnabledModules().isModuleEnabled ("juce_product_unlocking") @@ -1395,6 +1411,58 @@ private: } } + void writeDeviceInfoXML (const File& folder) const + { + const auto makeDeviceNode = [this] (XmlElement& devices) + { + auto* device = devices.createNewChildElement ("device"); + device->setAttribute ("manufacturer", project.getCompanyNameString()); + device->setAttribute ("product", projectName); + + const auto deviceName = (project.getCompanyNameString().isNotEmpty() ? (project.getCompanyNameString() + " ") : "") + + projectName; + device->setAttribute ("name", deviceName); + return device; + }; + + if (isVirtualMidiEnabled()) + { + { + auto path = folder.getChildFile ("app") + .getChildFile ("src") + .getChildFile ("main") + .getChildFile ("res") + .getChildFile ("xml") + .getChildFile ("juce_midi_virtual_ump.xml"); + + auto devices = std::make_unique ("devices"); + auto* device = makeDeviceNode (*devices); + auto* port = device->createNewChildElement ("port"); + port->setAttribute ("name", "MIDI 2.0"); + + writeXmlOrThrow (*devices, path, "utf-8", 100, true); + } + + { + auto path = folder.getChildFile ("app") + .getChildFile ("src") + .getChildFile ("main") + .getChildFile ("res") + .getChildFile ("xml") + .getChildFile ("juce_midi_virtual_bytestream.xml"); + + auto devices = std::make_unique ("devices"); + auto* device = makeDeviceNode (*devices); + auto* portIn = device->createNewChildElement ("input-port"); + portIn->setAttribute ("name", "In"); + auto* portOut = device->createNewChildElement ("output-port"); + portOut->setAttribute ("name", "Out"); + + writeXmlOrThrow (*devices, path, "utf-8", 100, true); + } + } + } + void writeAndroidManifest (const File& folder) const { std::unique_ptr manifest (createManifestXML()); @@ -1898,6 +1966,39 @@ private: metaData->setAttribute ("android:name", "firebase_analytics_collection_deactivated"); metaData->setAttribute ("android:value", "true"); } + + if (isVirtualMidiEnabled()) + { + { + auto* service = application.createNewChildElement ("service"); + service->setAttribute ("android:name", "com.rmsl.juce.VirtualMidiServices$VirtualUmpService"); + service->setAttribute ("android:enabled", "false"); + service->setAttribute ("android:exported", "true"); + service->setAttribute ("android:permission", "android.permission.BIND_MIDI_DEVICE_SERVICE"); + + auto* intentFilter = service->createNewChildElement ("intent-filter"); + intentFilter->createNewChildElement ("action")->setAttribute ("android:name", "android.media.midi.MidiUmpDeviceService"); + + auto* property = service->createNewChildElement ("property"); + property->setAttribute ("android:name", "android.media.midi.MidiUmpDeviceService"); + property->setAttribute ("android:resource", "@xml/juce_midi_virtual_ump"); + } + + { + auto* service = application.createNewChildElement ("service"); + service->setAttribute ("android:name", "com.rmsl.juce.VirtualMidiServices$VirtualBytestreamService"); + service->setAttribute ("android:enabled", "false"); + service->setAttribute ("android:exported", "true"); + service->setAttribute ("android:permission", "android.permission.BIND_MIDI_DEVICE_SERVICE"); + + auto* intentFilter = service->createNewChildElement ("intent-filter"); + intentFilter->createNewChildElement ("action")->setAttribute ("android:name", "android.media.midi.MidiDeviceService"); + + auto* metadata = service->createNewChildElement ("meta-data"); + metadata->setAttribute ("android:name", "android.media.midi.MidiDeviceService"); + metadata->setAttribute ("android:resource", "@xml/juce_midi_virtual_bytestream"); + } + } } void createProviderElement (XmlElement& application) const diff --git a/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h b/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h index cde686e0e1..62336c8b7b 100644 --- a/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h +++ b/extras/Projucer/Source/Utility/Helpers/jucer_PresetIDs.h @@ -268,6 +268,7 @@ namespace Ids DECLARE_ID (androidScreenOrientation); DECLARE_ID (androidExtraAssetsFolder); DECLARE_ID (androidStudioExePath); + DECLARE_ID (androidEnableVirtualMidi); DECLARE_ID (iosDeviceFamily); const Identifier iPhoneScreenOrientation ("iosScreenOrientation"); // old name is confusing DECLARE_ID (iPadScreenOrientation);