diff options
Diffstat (limited to 'java/org/direct_bt/BTFactory.java')
-rw-r--r-- | java/org/direct_bt/BTFactory.java | 530 |
1 files changed, 530 insertions, 0 deletions
diff --git a/java/org/direct_bt/BTFactory.java b/java/org/direct_bt/BTFactory.java new file mode 100644 index 00000000..cee2aa7f --- /dev/null +++ b/java/org/direct_bt/BTFactory.java @@ -0,0 +1,530 @@ +/** + * Author: Sven Gothel <[email protected]> + * Copyright (c) 2020 Gothel Software e.K. + * Copyright (c) 2020 ZAFENA AB + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package org.direct_bt; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +/** + * One stop {@link BTManager} API entry point. + * <p> + * Further provides access to certain property settings, + * see {@link #DEBUG}, {@link #VERBOSE}, {@link #DIRECTBT_CHARACTERISTIC_VALUE_CACHE_NOTIFICATION_COMPAT}. + * </p> + */ +public class BTFactory { + + /** + * Identifier names, allowing {@link BTFactory#getBluetoothManager(ImplementationIdentifier)} + * to initialize the required native libraries and to instantiate the root {@link BTManager} instance. + * <p> + * The implementation class must provide the static factory method + * <pre> + * public static synchronized BluetoothManager getBluetoothManager() throws BluetoothException { .. } + * </pre> + * </p> + */ + public static class ImplementationIdentifier { + /** + * Fully qualified class name for the {@link BTManager} implementation + * <p> + * The implementation class must provide the static factory method + * <pre> + * public static synchronized BluetoothManager getBluetoothManager() throws BluetoothException { .. } + * </pre> + * </p> + */ + public final String BluetoothManagerClassName; + /** Native library basename for the implementation native library */ + public final String ImplementationNativeLibraryBasename; + /** Native library basename for the Java binding native library */ + public final String JavaNativeLibraryBasename; + + public ImplementationIdentifier(final String BluetoothManagerClassName, + final String ImplementationNativeLibraryBasename, + final String JavaNativeLibraryBasename) { + this.BluetoothManagerClassName = BluetoothManagerClassName; + this.ImplementationNativeLibraryBasename = ImplementationNativeLibraryBasename; + this.JavaNativeLibraryBasename = JavaNativeLibraryBasename; + } + + /** + * <p> + * Implementation compares {@link #BluetoothManagerClassName} only for equality. + * </p> + * {@inheritDoc} + */ + @Override + public boolean equals(final Object other) { + if( null == other || !(other instanceof ImplementationIdentifier) ) { + return false; + } + final ImplementationIdentifier o = (ImplementationIdentifier)other; + return BluetoothManagerClassName.equals( o.BluetoothManagerClassName ); + } + + @Override + public String toString() { + return "ImplementationIdentifier[class "+BluetoothManagerClassName+ + ", implLib "+ImplementationNativeLibraryBasename+ + ", javaLib "+JavaNativeLibraryBasename+"]"; + } + } + + /** + * {@link ImplementationIdentifier} for D-Bus implementation: {@value} + * <p> + * This value is exposed for convenience, user implementations are welcome. + * </p> + */ + public static final ImplementationIdentifier DBusImplementationID = new ImplementationIdentifier("tinyb.dbus.DBusManager", "tinyb", "javatinyb"); + + /** + * {@link ImplementationIdentifier} for direct_bt implementation: {@value} + * <p> + * This value is exposed for convenience, user implementations are welcome. + * </p> + */ + public static final ImplementationIdentifier DirectBTImplementationID = new ImplementationIdentifier("jau.direct_bt.DBTManager", "direct_bt", "javadirect_bt"); + + private static final List<ImplementationIdentifier> implIDs = new ArrayList<ImplementationIdentifier>(); + + /** + * Manifest's {@link Attributes.Name#SPECIFICATION_VERSION} or {@code null} if not available. + */ + public static final String getAPIVersion() { return APIVersion; } + private static String APIVersion; + + /** + * Manifest's {@link Attributes.Name#IMPLEMENTATION_VERSION} or {@code null} if not available. + */ + public static final String getImplVersion() { return ImplVersion; } + private static String ImplVersion; + + /** + * Verbose logging enabled or disabled. + * <p> + * System property {@code org.tinyb.verbose}, boolean, default {@code false}. + * </p> + */ + public static final boolean VERBOSE; + + /** + * Debug logging enabled or disabled. + * <p> + * System property {@code org.tinyb.debug}, boolean, default {@code false}. + * </p> + */ + public static final boolean DEBUG; + + /** + * Default {@link BTMode} when initializing new adapter + * <p> + * System property {@code org.tinyb.btmode}, string, default {@code DUAL} {@link BTMode#DUAL}. + * </p> + * @since 2.0.0 + * @implNote not implemented in tinyb.dbus. + */ + public static final BTMode DEFAULT_BTMODE; + + static { + { + final String v = System.getProperty("org.tinyb.verbose", "false"); + VERBOSE = Boolean.valueOf(v); + } + { + final String v = System.getProperty("org.tinyb.debug", "false"); + DEBUG = Boolean.valueOf(v); + } + { + final String v = System.getProperty("org.tinyb.btmode", "DUAL"); + BTMode btMode = BTMode.DUAL; + try { + btMode = BTMode.get(v); + } catch (final IllegalArgumentException ex) { + System.err.println("Invalid BTMode '"+v+"': "+ex.getMessage()); + } + DEFAULT_BTMODE = btMode; + } + implIDs.add(DirectBTImplementationID); + implIDs.add(DBusImplementationID); + } + + private static ImplementationIdentifier initializedID = null; + + public static synchronized void checkInitialized() { + if( null == initializedID ) { + throw new IllegalStateException("BluetoothFactory not initialized."); + } + } + public static synchronized boolean isInitialized() { + return null == initializedID; + } + + private static synchronized void initLibrary(final ImplementationIdentifier id) { + if( null != initializedID ) { + if( id != initializedID ) { + throw new IllegalStateException("BluetoothFactory already initialized with "+initializedID+", can't override by "+id); + } + return; + } + + try { + final Throwable[] t = { null }; + if( !PlatformToolkit.loadLibrary(id.ImplementationNativeLibraryBasename, BTFactory.class.getClassLoader(), t) ) { + throw new RuntimeException("Couldn't load native library with basename <"+id.ImplementationNativeLibraryBasename+">", t[0]); + } + if( !PlatformToolkit.loadLibrary(id.JavaNativeLibraryBasename, BTFactory.class.getClassLoader(), t) ) { + throw new RuntimeException("Couldn't load native library with basename <"+id.JavaNativeLibraryBasename+">", t[0]); + } + } catch (final Throwable e) { + e.printStackTrace(); + throw e; // fwd exception - end here + } + + // Map all Java properties '[org.]tinyb.*' and 'direct_bt.*' to native environment. + try { + if( DEBUG ) { + System.err.println("BlootoothFactory: Mapping '[org.]tinyb.*' and 'direct_bt.*' properties to native environment"); + } + final Properties props = AccessController.doPrivileged(new PrivilegedAction<Properties>() { + @Override + public Properties run() { + return System.getProperties(); + } }); + + final Enumeration<?> enums = props.propertyNames(); + while (enums.hasMoreElements()) { + final String key = (String) enums.nextElement(); + if( key.startsWith("org.tinyb.") || + key.startsWith("direct_bt.") || key.startsWith("tinyb.") ) + { + final String value = props.getProperty(key); + if( DEBUG ) { + System.err.println(" <"+key+"> := <"+value+">"); + } + setenv(key, value, true /* overwrite */); + } + } + } catch (final Throwable e) { + System.err.println("Caught exception while forwarding system properties: "+e.getMessage()); + e.printStackTrace(); + } + + try { + final Manifest manifest = getManifest(BTFactory.class.getClassLoader(), new String[] { "org.tinyb" } ); + final Attributes mfAttributes = null != manifest ? manifest.getMainAttributes() : null; + + // major.minor must match! + final String NAPIVersion = getNativeAPIVersion(); + final String JAPIVersion = null != mfAttributes ? mfAttributes.getValue(Attributes.Name.SPECIFICATION_VERSION) : null; + if ( null != JAPIVersion && JAPIVersion.equals(NAPIVersion) == false) { + final String[] NAPIVersionCode = NAPIVersion.split("\\D"); + final String[] JAPIVersionCode = JAPIVersion.split("\\D"); + if (JAPIVersionCode[0].equals(NAPIVersionCode[0]) == false) { + if (Integer.valueOf(JAPIVersionCode[0]) < Integer.valueOf(NAPIVersionCode[0])) { + throw new RuntimeException("Java library "+JAPIVersion+" < native library "+NAPIVersion+". Please update the Java library."); + } else { + throw new RuntimeException("Native library "+NAPIVersion+" < java library "+JAPIVersion+". Please update the native library."); + } + } else if (JAPIVersionCode[1].equals(NAPIVersionCode[1]) == false) { + if (Integer.valueOf(JAPIVersionCode[1]) < Integer.valueOf(NAPIVersionCode[1])) { + throw new RuntimeException("Java library "+JAPIVersion+" < native library "+NAPIVersion+". Please update the Java library."); + } else { + throw new RuntimeException("Native library "+NAPIVersion+" < java library "+JAPIVersion+". Please update the native library."); + } + } + } + initializedID = id; // initialized! + + APIVersion = JAPIVersion; + ImplVersion = null != mfAttributes ? mfAttributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION) : null; + if( VERBOSE ) { + System.err.println("tinyb2 loaded "+id); + System.err.println("tinyb2 java api version "+JAPIVersion); + System.err.println("tinyb2 native api version "+NAPIVersion); + if( null != mfAttributes ) { + final Attributes.Name[] versionAttributeNames = new Attributes.Name[] { + Attributes.Name.SPECIFICATION_TITLE, + Attributes.Name.SPECIFICATION_VENDOR, + Attributes.Name.SPECIFICATION_VERSION, + Attributes.Name.IMPLEMENTATION_TITLE, + Attributes.Name.IMPLEMENTATION_VENDOR, + Attributes.Name.IMPLEMENTATION_VERSION, + new Attributes.Name("Implementation-Commit") }; + for( final Attributes.Name an : versionAttributeNames ) { + System.err.println(" "+an+": "+mfAttributes.getValue(an)); + } + } else { + System.err.println(" No Manifest available;"); + } + } + } catch (final Throwable e) { + System.err.println("Error querying manifest information."); + e.printStackTrace(); + throw e; // fwd exception - end here + } + } + + private static synchronized BTManager getBluetoothManager(final Class<?> factoryImplClass) + throws BTException, NoSuchMethodException, SecurityException, + IllegalAccessException, IllegalArgumentException, InvocationTargetException + { + final Method m = factoryImplClass.getMethod("getManager"); + return (BTManager)m.invoke(null); + } + + /** + * Registers a new {@link ImplementationIdentifier} to the internal list. + * The {@code id} is only added if not registered yet. + * @param id the {@link ImplementationIdentifier} to register + * @return {@code true} if the given {@link ImplementationIdentifier} has been newly added, + * otherwise {@code false}. + */ + public static synchronized boolean registerImplementationIdentifier(final ImplementationIdentifier id) { + if( null == id ) { + return false; + } + if( implIDs.contains(id) ) { + return false; + } + return implIDs.add(id); + } + + /** + * Returns the matching {@link ImplementationIdentifier} from the internal list or {@code null} if not found. + * @param fqBluetoothManagerImplementationClassName fully qualified class name for the {@link BTManager} implementation + */ + public static synchronized ImplementationIdentifier getImplementationIdentifier(final String fqBluetoothManagerImplementationClassName) { + for(final ImplementationIdentifier id : implIDs) { + if( id.BluetoothManagerClassName.equals(fqBluetoothManagerImplementationClassName) ) { + return id; + } + } + return null; + } + + /** + * Returns an initialized BluetoothManager instance using the given {@code fqBluetoothManagerImplementationClassName} + * to lookup a registered {@link ImplementationIdentifier}. + * <p> + * If found, method returns {@link #getBluetoothManager(ImplementationIdentifier)}, otherwise {@code null}. + * </p> + * <p> + * The chosen implementation can't be changed within a running implementation, an exception is thrown if tried. + * </p> + * + * @param fqBluetoothManagerImplementationClassName fully qualified class name for the {@link BTManager} implementation + * @throws BTException + * @throws NoSuchMethodException + * @throws SecurityException + * @throws IllegalAccessException + * @throws IllegalArgumentException + * @throws InvocationTargetException + * @throws ClassNotFoundException + * @see {@link #DBusFactoryImplClassName} + * @see {@link #DirectBTFactoryImplClassName} + */ + public static synchronized BTManager getBluetoothManager(final String fqBluetoothManagerImplementationClassName) + throws BTException, NoSuchMethodException, SecurityException, + IllegalAccessException, IllegalArgumentException, InvocationTargetException, ClassNotFoundException + { + final ImplementationIdentifier id = getImplementationIdentifier(fqBluetoothManagerImplementationClassName); + if( null != id ) { + return getBluetoothManager(id); + } + return null; + } + + /** + * Returns an initialized BluetoothManager instance using the given {@link ImplementationIdentifier}. + * <p> + * If the {@link ImplementationIdentifier} has not been {@link #registerImplementationIdentifier(ImplementationIdentifier)}, + * it will be added to the list. + * </p> + * <p> + * The chosen implementation can't be changed within a running implementation, an exception is thrown if tried. + * </p> + * @param id the specific {@link ImplementationIdentifier} + * @throws BTException + * @throws NoSuchMethodException + * @throws SecurityException + * @throws IllegalAccessException + * @throws IllegalArgumentException + * @throws InvocationTargetException + * @throws ClassNotFoundException + * @see {@link #DBusFactoryImplClassName} + * @see {@link #DirectBTFactoryImplClassName} + */ + public static synchronized BTManager getBluetoothManager(final ImplementationIdentifier id) + throws BTException, NoSuchMethodException, SecurityException, + IllegalAccessException, IllegalArgumentException, InvocationTargetException, ClassNotFoundException + { + registerImplementationIdentifier(id); + initLibrary(id); + final Class<?> factoryImpl = Class.forName(id.BluetoothManagerClassName); + return getBluetoothManager(factoryImpl); + } + + /** + * Returns an initialized BluetoothManager instance using a D-Bus implementation. + * <p> + * Issues {@link #getBluetoothManager(ImplementationIdentifier)} using {@link #DBusImplementationID}. + * </p> + * <p> + * The chosen implementation can't be changed within a running implementation, an exception is thrown if tried. + * </p> + * @throws BTException + * @throws NoSuchMethodException + * @throws SecurityException + * @throws IllegalAccessException + * @throws IllegalArgumentException + * @throws InvocationTargetException + * @throws ClassNotFoundException + */ + public static synchronized BTManager getDBusBluetoothManager() + throws BTException, NoSuchMethodException, SecurityException, + IllegalAccessException, IllegalArgumentException, InvocationTargetException, ClassNotFoundException + { + return getBluetoothManager(DBusImplementationID); + } + + /** + * Returns an initialized BluetoothManager instance using the DirectBT implementation. + * <p> + * Issues {@link #getBluetoothManager(ImplementationIdentifier)} using {@link #DirectBTImplementationID}. + * </p> + * <p> + * The chosen implementation can't be changed within a running implementation, an exception is thrown if tried. + * </p> + * @throws BTException + * @throws NoSuchMethodException + * @throws SecurityException + * @throws IllegalAccessException + * @throws IllegalArgumentException + * @throws InvocationTargetException + * @throws ClassNotFoundException + */ + public static synchronized BTManager getDirectBTBluetoothManager() + throws BTException, NoSuchMethodException, SecurityException, + IllegalAccessException, IllegalArgumentException, InvocationTargetException, ClassNotFoundException + { + return getBluetoothManager(DirectBTImplementationID); + } + + private static final Manifest getManifest(final ClassLoader cl, final String[] extensions) { + final Manifest[] extManifests = new Manifest[extensions.length]; + try { + final Enumeration<URL> resources = cl.getResources("META-INF/MANIFEST.MF"); + while (resources.hasMoreElements()) { + final URL resURL = resources.nextElement(); + if( DEBUG ) { + System.err.println("resource: "+resURL); + } + final InputStream is = resURL.openStream(); + final Manifest manifest; + try { + manifest = new Manifest(is); + } finally { + try { + is.close(); + } catch (final IOException e) {} + } + final Attributes attributes = manifest.getMainAttributes(); + if(attributes != null) { + final String attributesExtName = attributes.getValue( Attributes.Name.EXTENSION_NAME ); + if( DEBUG ) { + System.err.println("resource: "+resURL+", attributes extName "+attributesExtName+", count "+attributes.size()); + final Set<Object> keys = attributes.keySet(); + for(final Iterator<Object> iter=keys.iterator(); iter.hasNext(); ) { + final Attributes.Name key = (Attributes.Name) iter.next(); + final String val = attributes.getValue(key); + System.err.println(" "+key+": "+val); + } + } + for(int i=0; i < extensions.length && null == extManifests[i]; i++) { + final String extension = extensions[i]; + if( extension.equals( attributesExtName ) ) { + if( 0 == i ) { + return manifest; // 1st one has highest prio - done + } + extManifests[i] = manifest; + } + } + } + } + } catch (final IOException ex) { + throw new RuntimeException("Unable to read manifest.", ex); + } + for(int i=1; i<extManifests.length; i++) { + if( null != extManifests[i] ) { + return extManifests[i]; + } + } + return null; + } + + private native static String getNativeAPIVersion(); + private native static void setenv(String name, String value, boolean overwrite); +} + +/** \example DBTScanner10.java + * This Java scanner example uses the Direct-BT fully event driven workflow + * and adds multithreading, i.e. one thread processes each found device found + * as notified via the event listener. + * <p> + * This example represents the recommended utilization of Direct-BT. + * </p> + */ + +/** \example ScannerTinyB00.java + * This Java scanner example is a TinyB backward compatible and not fully event driven. + * It simply polls found devices and shows certain results. + * <p> + * This example does not represent the recommended utilization of Direct-BT. + * </p> + */ + +/** \example ScannerTinyB01.java + * This Java scanner example is a TinyB backward compatible and not fully event driven. + * It simply polls found devices and shows certain results. + * However, the AdapterStatusListener is attached if supported. + * <p> + * This example does not represent the recommended utilization of Direct-BT. + * </p> + */ |