| /* |
| * BRLTTY - A background process providing access to the console screen (when in |
| * text mode) for a blind person using a refreshable braille display. |
| * |
| * Copyright (C) 1995-2023 by The BRLTTY Developers. |
| * |
| * BRLTTY comes with ABSOLUTELY NO WARRANTY. |
| * |
| * This is free software, placed under the terms of the |
| * GNU Lesser General Public License, as published by the Free Software |
| * Foundation; either version 2.1 of the License, or (at your option) any |
| * later version. Please see the file LICENSE-LGPL for details. |
| * |
| * Web Page: http://brltty.app/ |
| * |
| * This software is maintained by Dave Mielke <dave@mielke.cc>. |
| */ |
| |
| #include "prologue.h" |
| |
| #include <string.h> |
| #include <errno.h> |
| |
| #include "log.h" |
| #include "io_bluetooth.h" |
| #include "bluetooth_internal.h" |
| #include "async_handle.h" |
| #include "async_io.h" |
| #include "io_misc.h" |
| #include "thread.h" |
| #include "system_java.h" |
| |
| static jclass connectionClass = NULL; |
| static jmethodID connectionConstructor = 0; |
| static jmethodID canDiscoverMethod = 0; |
| static jmethodID openMethod = 0; |
| static jmethodID closeMethod = 0; |
| static jmethodID writeMethod = 0; |
| |
| static int |
| bthGetConnectionClass (JNIEnv *env) { |
| return findJavaClass( |
| env, &connectionClass, JAVA_OBJ_BRLTTY("BluetoothConnection") |
| ); |
| } |
| |
| static int |
| bthGetConnectionConstructor (JNIEnv *env) { |
| return findJavaConstructor( |
| env, &connectionConstructor, connectionClass, |
| JAVA_SIG_CONSTRUCTOR( |
| JAVA_SIG_LONG // address |
| ) |
| ); |
| } |
| |
| static int |
| bthGetCanDiscoverMethod (JNIEnv *env) { |
| return findJavaInstanceMethod( |
| env, &canDiscoverMethod, connectionClass, "canDiscover", |
| JAVA_SIG_METHOD(JAVA_SIG_BOOLEAN, |
| ) |
| ); |
| } |
| |
| static int |
| bthGetOpenMethod (JNIEnv *env) { |
| return findJavaInstanceMethod( |
| env, &openMethod, connectionClass, "open", |
| JAVA_SIG_METHOD(JAVA_SIG_BOOLEAN, |
| JAVA_SIG_INT // inputPipe |
| JAVA_SIG_INT // channel |
| JAVA_SIG_BOOLEAN // secure |
| ) |
| ); |
| } |
| |
| static int |
| bthGetCloseMethod (JNIEnv *env) { |
| return findJavaInstanceMethod( |
| env, &closeMethod, connectionClass, "close", |
| JAVA_SIG_METHOD(JAVA_SIG_VOID, |
| ) |
| ); |
| } |
| |
| static int |
| bthGetWriteMethod (JNIEnv *env) { |
| return (findJavaInstanceMethod( |
| env, &writeMethod, connectionClass, "write", |
| JAVA_SIG_METHOD(JAVA_SIG_BOOLEAN, |
| JAVA_SIG_ARRAY(JAVA_SIG_BYTE)) // bytes |
| ) |
| ); |
| } |
| |
| struct BluetoothConnectionExtensionStruct { |
| JNIEnv *env; |
| |
| jobject connection; |
| AsyncHandle inputMonitor; |
| int inputPipe[2]; |
| }; |
| |
| BluetoothConnectionExtension * |
| bthNewConnectionExtension (uint64_t bda) { |
| BluetoothConnectionExtension *bcx; |
| |
| if ((bcx = malloc(sizeof(*bcx)))) { |
| memset(bcx, 0, sizeof(*bcx)); |
| |
| bcx->inputPipe[0] = INVALID_FILE_DESCRIPTOR; |
| bcx->inputPipe[1] = INVALID_FILE_DESCRIPTOR; |
| |
| if ((bcx->env = getJavaNativeInterface())) { |
| if (bthGetConnectionClass(bcx->env)) { |
| if (bthGetConnectionConstructor(bcx->env)) { |
| jobject localReference = (*bcx->env)->NewObject(bcx->env, connectionClass, connectionConstructor, bda); |
| |
| if (!clearJavaException(bcx->env, 1)) { |
| jobject globalReference = (*bcx->env)->NewGlobalRef(bcx->env, localReference); |
| |
| (*bcx->env)->DeleteLocalRef(bcx->env, localReference); |
| localReference = NULL; |
| |
| if (globalReference) { |
| bcx->connection = globalReference; |
| return bcx; |
| } else { |
| logMallocError(); |
| clearJavaException(bcx->env, 0); |
| } |
| } |
| } |
| } |
| } |
| |
| free(bcx); |
| } else { |
| logMallocError(); |
| } |
| |
| return NULL; |
| } |
| |
| static void |
| bthCancelInputMonitor (BluetoothConnectionExtension *bcx) { |
| if (bcx->inputMonitor) { |
| asyncCancelRequest(bcx->inputMonitor); |
| bcx->inputMonitor = NULL; |
| } |
| } |
| |
| void |
| bthReleaseConnectionExtension (BluetoothConnectionExtension *bcx) { |
| bthCancelInputMonitor(bcx); |
| |
| if (bcx->connection) { |
| if (bthGetCloseMethod(bcx->env)) { |
| (*bcx->env)->CallVoidMethod(bcx->env, bcx->connection, closeMethod); |
| } |
| |
| (*bcx->env)->DeleteGlobalRef(bcx->env, bcx->connection); |
| clearJavaException(bcx->env, 1); |
| } |
| |
| closeFile(&bcx->inputPipe[0]); |
| closeFile(&bcx->inputPipe[1]); |
| |
| free(bcx); |
| } |
| |
| typedef struct { |
| BluetoothConnectionExtension *const bcx; |
| uint8_t const channel; |
| int const timeout; |
| |
| int error; |
| } OpenBluetoothConnectionData; |
| |
| THREAD_FUNCTION(runOpenBluetoothConnection) { |
| OpenBluetoothConnectionData *obc = argument; |
| JNIEnv *env; |
| |
| if ((env = getJavaNativeInterface())) { |
| if (pipe(obc->bcx->inputPipe) != -1) { |
| if (setBlockingIo(obc->bcx->inputPipe[0], 0)) { |
| if (bthGetOpenMethod(env)) { |
| jboolean result = (*env)->CallBooleanMethod(env, obc->bcx->connection, openMethod, |
| obc->bcx->inputPipe[1], obc->channel, JNI_FALSE); |
| |
| if (!clearJavaException(env, 1)) { |
| if (result == JNI_TRUE) { |
| closeFile(&obc->bcx->inputPipe[1]); |
| obc->error = 0; |
| goto done; |
| } |
| } |
| |
| errno = EIO; |
| } |
| } |
| |
| closeFile(&obc->bcx->inputPipe[0]); |
| closeFile(&obc->bcx->inputPipe[1]); |
| } else { |
| logSystemError("pipe"); |
| } |
| } |
| |
| obc->error = errno; |
| done: |
| return NULL; |
| } |
| |
| int |
| bthOpenChannel (BluetoothConnectionExtension *bcx, uint8_t channel, int timeout) { |
| OpenBluetoothConnectionData obc = { |
| .bcx = bcx, |
| .channel = channel, |
| .timeout = timeout, |
| |
| .error = EIO |
| }; |
| |
| if (callThreadFunction("bluetooth-open", runOpenBluetoothConnection, &obc, NULL)) { |
| if (!obc.error) return 1; |
| errno = obc.error; |
| } |
| |
| return 0; |
| } |
| |
| int |
| bthDiscoverChannel ( |
| uint8_t *channel, BluetoothConnectionExtension *bcx, |
| const void *uuidBytes, size_t uuidLength, |
| int timeout |
| ) { |
| JNIEnv *env = bcx->env; |
| |
| if (bthGetCanDiscoverMethod(env)) { |
| jboolean result = (*env)->CallBooleanMethod(env, bcx->connection, canDiscoverMethod); |
| |
| if (!clearJavaException(env, 1)) { |
| int yes = result == JNI_TRUE; |
| |
| if (yes) { |
| logMessage(LOG_CATEGORY(BLUETOOTH_IO), "can discover serial port channel"); |
| *channel = 0; |
| } else { |
| errno = ENOENT; |
| } |
| |
| return yes; |
| } |
| } |
| |
| errno = EIO; |
| return 0; |
| } |
| |
| int |
| bthMonitorInput (BluetoothConnection *connection, AsyncMonitorCallback *callback, void *data) { |
| BluetoothConnectionExtension *bcx = connection->extension; |
| |
| bthCancelInputMonitor(bcx); |
| if (!callback) return 1; |
| return asyncMonitorFileInput(&bcx->inputMonitor, bcx->inputPipe[0], callback, data); |
| } |
| |
| int |
| bthPollInput (BluetoothConnectionExtension *bcx, int timeout) { |
| return awaitFileInput(bcx->inputPipe[0], timeout); |
| } |
| |
| ssize_t |
| bthGetData ( |
| BluetoothConnectionExtension *bcx, void *buffer, size_t size, |
| int initialTimeout, int subsequentTimeout |
| ) { |
| return readFile(bcx->inputPipe[0], buffer, size, initialTimeout, subsequentTimeout); |
| } |
| |
| ssize_t |
| bthPutData (BluetoothConnectionExtension *bcx, const void *buffer, size_t size) { |
| if (bthGetWriteMethod(bcx->env)) { |
| jbyteArray bytes = (*bcx->env)->NewByteArray(bcx->env, size); |
| |
| if (bytes) { |
| jboolean result; |
| |
| (*bcx->env)->SetByteArrayRegion(bcx->env, bytes, 0, size, buffer); |
| result = (*bcx->env)->CallBooleanMethod(bcx->env, bcx->connection, writeMethod, bytes); |
| (*bcx->env)->DeleteLocalRef(bcx->env, bytes); |
| |
| if (!clearJavaException(bcx->env, 1)) { |
| if (result == JNI_TRUE) { |
| return size; |
| } |
| } |
| |
| errno = EIO; |
| } else { |
| errno = ENOMEM; |
| } |
| } else { |
| errno = ENOSYS; |
| } |
| |
| logSystemError("Bluetooth write"); |
| return -1; |
| } |
| |
| char * |
| bthObtainDeviceName (uint64_t bda, int timeout) { |
| char *name = NULL; |
| JNIEnv *env = getJavaNativeInterface(); |
| |
| if (env) { |
| if (bthGetConnectionClass(env)) { |
| static jmethodID method = 0; |
| |
| if (findJavaStaticMethod(env, &method, connectionClass, "getName", |
| JAVA_SIG_METHOD(JAVA_SIG_STRING, |
| JAVA_SIG_LONG // address |
| ))) { |
| jstring jName = (*env)->CallStaticObjectMethod(env, connectionClass, method, bda); |
| |
| if (jName) { |
| const char *cName = (*env)->GetStringUTFChars(env, jName, NULL); |
| |
| if (cName) { |
| if (!(name = strdup(cName))) logMallocError(); |
| (*env)->ReleaseStringUTFChars(env, jName, cName); |
| } else { |
| logMallocError(); |
| clearJavaException(env, 0); |
| } |
| |
| (*env)->DeleteLocalRef(env, jName); |
| } else { |
| logMallocError(); |
| clearJavaException(env, 0); |
| } |
| } |
| } |
| } |
| |
| return name; |
| } |
| |
| static jmethodID getPairedDeviceCountMethod = 0; |
| static jmethodID getPairedDeviceAddressMethod = 0; |
| static jmethodID getPairedDeviceNameMethod = 0; |
| |
| static int |
| bthGetPairedDeviceCountMethod (JNIEnv *env) { |
| return findJavaStaticMethod( |
| env, &getPairedDeviceCountMethod, connectionClass, "getPairedDeviceCount", |
| JAVA_SIG_METHOD(JAVA_SIG_INT, |
| ) |
| ); |
| } |
| |
| static int |
| bthGetPairedDeviceAddressMethod (JNIEnv *env) { |
| return findJavaStaticMethod( |
| env, &getPairedDeviceAddressMethod, connectionClass, "getPairedDeviceAddress", |
| JAVA_SIG_METHOD(JAVA_SIG_STRING, |
| JAVA_SIG_INT // index |
| ) |
| ); |
| } |
| |
| static int |
| bthGetPairedDeviceNameMethod (JNIEnv *env) { |
| return findJavaStaticMethod( |
| env, &getPairedDeviceNameMethod, connectionClass, "getPairedDeviceName", |
| JAVA_SIG_METHOD(JAVA_SIG_STRING, |
| JAVA_SIG_INT // index |
| ) |
| ); |
| } |
| |
| static JNIEnv * |
| bthGetPairedDeviceMethods (void) { |
| JNIEnv *env = getJavaNativeInterface(); |
| |
| if (env) { |
| if (bthGetConnectionClass(env)) { |
| if (bthGetPairedDeviceCountMethod(env)) { |
| if (bthGetPairedDeviceAddressMethod(env)) { |
| if (bthGetPairedDeviceNameMethod(env)) { |
| return env; |
| } |
| } |
| } |
| } |
| } |
| |
| return NULL; |
| } |
| |
| void |
| bthProcessDiscoveredDevices ( |
| DiscoveredBluetoothDeviceTester *testDevice, void *data |
| ) { |
| JNIEnv *env = bthGetPairedDeviceMethods(); |
| |
| if (env) { |
| jint count = (*env)->CallStaticIntMethod(env, connectionClass, getPairedDeviceCountMethod); |
| |
| for (jint index=0; index<count; index+=1) { |
| int found = 0; |
| jstring jAddress = (*env)->CallStaticObjectMethod(env, connectionClass, getPairedDeviceAddressMethod, index); |
| |
| if (jAddress) { |
| const char *cAddress = (*env)->GetStringUTFChars(env, jAddress, NULL); |
| |
| if (cAddress) { |
| uint64_t address; |
| |
| if (bthParseAddress(&address, cAddress)) { |
| jstring jName = (*env)->CallStaticObjectMethod(env, connectionClass, getPairedDeviceNameMethod, index); |
| const char *cName = jName? (*env)->GetStringUTFChars(env, jName, NULL): NULL; |
| |
| const DiscoveredBluetoothDevice device = { |
| .address = address, |
| .name = cName, |
| .paired = 1 |
| }; |
| |
| if (testDevice(&device, data)) found = 1; |
| if (cName) (*env)->ReleaseStringUTFChars(env, jName, cName); |
| if (jName) (*env)->DeleteLocalRef(env, jName); |
| } |
| |
| (*env)->ReleaseStringUTFChars(env, jAddress, cAddress); |
| } |
| |
| (*env)->DeleteLocalRef(env, jAddress); |
| } |
| |
| if (found) break; |
| } |
| } |
| } |