1 /*************************************************************************/
2 /*  GodotPluginRegistry.java                                             */
3 /*************************************************************************/
4 /*                       This file is part of:                           */
5 /*                           GODOT ENGINE                                */
6 /*                      https://godotengine.org                          */
7 /*************************************************************************/
8 /* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */
9 /* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */
10 /*                                                                       */
11 /* Permission is hereby granted, free of charge, to any person obtaining */
12 /* a copy of this software and associated documentation files (the       */
13 /* "Software"), to deal in the Software without restriction, including   */
14 /* without limitation the rights to use, copy, modify, merge, publish,   */
15 /* distribute, sublicense, and/or sell copies of the Software, and to    */
16 /* permit persons to whom the Software is furnished to do so, subject to */
17 /* the following conditions:                                             */
18 /*                                                                       */
19 /* The above copyright notice and this permission notice shall be        */
20 /* included in all copies or substantial portions of the Software.       */
21 /*                                                                       */
22 /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
23 /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
24 /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
25 /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
26 /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
27 /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
28 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
29 /*************************************************************************/
30 
31 package org.godotengine.godot.plugin;
32 
33 import org.godotengine.godot.Godot;
34 
35 import android.content.pm.ApplicationInfo;
36 import android.content.pm.PackageManager;
37 import android.os.Bundle;
38 import android.text.TextUtils;
39 import android.util.Log;
40 
41 import androidx.annotation.Nullable;
42 
43 import java.lang.reflect.Constructor;
44 import java.lang.reflect.InvocationTargetException;
45 import java.util.Collection;
46 import java.util.HashSet;
47 import java.util.Set;
48 import java.util.concurrent.ConcurrentHashMap;
49 
50 /**
51  * Registry used to load and access the registered Godot Android plugins.
52  */
53 public final class GodotPluginRegistry {
54 
55 	private static final String TAG = GodotPluginRegistry.class.getSimpleName();
56 
57 	private static final String GODOT_PLUGIN_V1_NAME_PREFIX = "org.godotengine.plugin.v1.";
58 
59 	/**
60 	 * Name for the metadata containing the list of Godot plugins to enable.
61 	 */
62 	private static final String GODOT_ENABLED_PLUGINS_LABEL = "plugins";
63 
64 	private static final String PLUGIN_VALUE_SEPARATOR_REGEX = "\\|";
65 
66 	private static GodotPluginRegistry instance;
67 	private final ConcurrentHashMap<String, GodotPlugin> registry;
68 
GodotPluginRegistry(Godot godot)69 	private GodotPluginRegistry(Godot godot) {
70 		registry = new ConcurrentHashMap<>();
71 		loadPlugins(godot);
72 	}
73 
74 	/**
75 	 * Retrieve the plugin tied to the given plugin name.
76 	 * @param pluginName Name of the plugin
77 	 * @return {@link GodotPlugin} handle if it exists, null otherwise.
78 	 */
79 	@Nullable
getPlugin(String pluginName)80 	public GodotPlugin getPlugin(String pluginName) {
81 		return registry.get(pluginName);
82 	}
83 
84 	/**
85 	 * Retrieve the full set of loaded plugins.
86 	 */
getAllPlugins()87 	public Collection<GodotPlugin> getAllPlugins() {
88 		return registry.values();
89 	}
90 
91 	/**
92 	 * Parse the manifest file and load all included Godot Android plugins.
93 	 * <p>
94 	 * A plugin manifest entry is a '<meta-data>' tag setup as described in the {@link GodotPlugin}
95 	 * documentation.
96 	 *
97 	 * @param godot Godot instance
98 	 * @return A singleton instance of {@link GodotPluginRegistry}. This ensures that only one instance
99 	 * of each Godot Android plugins is available at runtime.
100 	 */
initializePluginRegistry(Godot godot)101 	public static GodotPluginRegistry initializePluginRegistry(Godot godot) {
102 		if (instance == null) {
103 			instance = new GodotPluginRegistry(godot);
104 		}
105 
106 		return instance;
107 	}
108 
109 	/**
110 	 * Return the plugin registry if it's initialized.
111 	 * Throws a {@link IllegalStateException} exception if not.
112 	 *
113 	 * @throws IllegalStateException if {@link GodotPluginRegistry#initializePluginRegistry(Godot)} has not been called prior to calling this method.
114 	 */
getPluginRegistry()115 	public static GodotPluginRegistry getPluginRegistry() throws IllegalStateException {
116 		if (instance == null) {
117 			throw new IllegalStateException("Plugin registry hasn't been initialized.");
118 		}
119 
120 		return instance;
121 	}
122 
loadPlugins(Godot godot)123 	private void loadPlugins(Godot godot) {
124 		try {
125 			ApplicationInfo appInfo = godot
126 											  .getPackageManager()
127 											  .getApplicationInfo(godot.getPackageName(), PackageManager.GET_META_DATA);
128 			Bundle metaData = appInfo.metaData;
129 			if (metaData == null || metaData.isEmpty()) {
130 				return;
131 			}
132 
133 			// When using the Godot editor for building and exporting the apk, this is used to check
134 			// which plugins to enable.
135 			// When using a custom process to generate the apk, the metadata is not needed since
136 			// it's assumed that the developer is aware of the dependencies included in the apk.
137 			final Set<String> enabledPluginsSet;
138 			if (metaData.containsKey(GODOT_ENABLED_PLUGINS_LABEL)) {
139 				String enabledPlugins = metaData.getString(GODOT_ENABLED_PLUGINS_LABEL, "");
140 				String[] enabledPluginsList = enabledPlugins.split(PLUGIN_VALUE_SEPARATOR_REGEX);
141 				if (enabledPluginsList.length == 0) {
142 					// No plugins to enable. Aborting early.
143 					return;
144 				}
145 
146 				enabledPluginsSet = new HashSet<>();
147 				for (String enabledPlugin : enabledPluginsList) {
148 					enabledPluginsSet.add(enabledPlugin.trim());
149 				}
150 			} else {
151 				enabledPluginsSet = null;
152 			}
153 
154 			int godotPluginV1NamePrefixLength = GODOT_PLUGIN_V1_NAME_PREFIX.length();
155 			for (String metaDataName : metaData.keySet()) {
156 				// Parse the meta-data looking for entry with the Godot plugin name prefix.
157 				if (metaDataName.startsWith(GODOT_PLUGIN_V1_NAME_PREFIX)) {
158 					String pluginName = metaDataName.substring(godotPluginV1NamePrefixLength).trim();
159 					if (enabledPluginsSet != null && !enabledPluginsSet.contains(pluginName)) {
160 						Log.w(TAG, "Plugin " + pluginName + " is listed in the dependencies but is not enabled.");
161 						continue;
162 					}
163 
164 					Log.i(TAG, "Initializing Godot plugin " + pluginName);
165 
166 					// Retrieve the plugin class full name.
167 					String pluginHandleClassFullName = metaData.getString(metaDataName);
168 					if (!TextUtils.isEmpty(pluginHandleClassFullName)) {
169 						try {
170 							// Attempt to create the plugin init class via reflection.
171 							@SuppressWarnings("unchecked")
172 							Class<GodotPlugin> pluginClass = (Class<GodotPlugin>)Class
173 																	 .forName(pluginHandleClassFullName);
174 							Constructor<GodotPlugin> pluginConstructor = pluginClass
175 																				 .getConstructor(Godot.class);
176 							GodotPlugin pluginHandle = pluginConstructor.newInstance(godot);
177 
178 							// Load the plugin initializer into the registry using the plugin name
179 							// as key.
180 							if (!pluginName.equals(pluginHandle.getPluginName())) {
181 								Log.w(TAG,
182 										"Meta-data plugin name does not match the value returned by the plugin handle: " + pluginName + " =/= " + pluginHandle.getPluginName());
183 							}
184 							registry.put(pluginName, pluginHandle);
185 							Log.i(TAG, "Completed initialization for Godot plugin " + pluginHandle.getPluginName());
186 						} catch (ClassNotFoundException e) {
187 							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
188 						} catch (IllegalAccessException e) {
189 							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
190 						} catch (InstantiationException e) {
191 							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
192 						} catch (NoSuchMethodException e) {
193 							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
194 						} catch (InvocationTargetException e) {
195 							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
196 						}
197 					} else {
198 						Log.w(TAG, "Invalid plugin loader class for " + pluginName);
199 					}
200 				}
201 			}
202 		} catch (PackageManager.NameNotFoundException e) {
203 			Log.e(TAG, "Unable load Godot Android plugins from the manifest file.", e);
204 		}
205 	}
206 }
207