/* ** Copyright 2008, The Android Open Source Project ** ** Licensed under the Apache License, Version 2.0 (the "License"); ** you may not use this file except in compliance with the License. ** You may obtain a copy of the License at ** ** http://www.apache.org/licenses/LICENSE-2.0 ** ** Unless required by applicable law or agreed to in writing, software ** distributed under the License is distributed on an "AS IS" BASIS, ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ** See the License for the specific language governing permissions and ** limitations under the License. */ #define LOG_TAG "BluetoothEventLoop.cpp" #include "android_bluetooth_common.h" #include "android_runtime/AndroidRuntime.h" #include "cutils/sockets.h" #include "JNIHelp.h" #include "jni.h" #include "utils/Log.h" #include "utils/misc.h" #include #include #include #include #include #ifdef HAVE_BLUETOOTH #include #endif namespace android { #define CREATE_DEVICE_ALREADY_EXISTS 1 #define CREATE_DEVICE_SUCCESS 0 #define CREATE_DEVICE_FAILED -1 #ifdef HAVE_BLUETOOTH static jfieldID field_mNativeData; static jmethodID method_onPropertyChanged; static jmethodID method_onDevicePropertyChanged; static jmethodID method_onDeviceFound; static jmethodID method_onDeviceDisappeared; static jmethodID method_onDeviceCreated; static jmethodID method_onDeviceRemoved; static jmethodID method_onDeviceDisconnectRequested; static jmethodID method_onNetworkDeviceDisconnected; static jmethodID method_onNetworkDeviceConnected; static jmethodID method_onCreatePairedDeviceResult; static jmethodID method_onCreateDeviceResult; static jmethodID method_onDiscoverServicesResult; static jmethodID method_onGetDeviceServiceChannelResult; static jmethodID method_onRequestPinCode; static jmethodID method_onRequestPasskey; static jmethodID method_onRequestPasskeyConfirmation; static jmethodID method_onRequestPairingConsent; static jmethodID method_onDisplayPasskey; static jmethodID method_onRequestOobData; static jmethodID method_onAgentOutOfBandDataAvailable; static jmethodID method_onAgentAuthorize; static jmethodID method_onAgentCancel; static jmethodID method_onInputDevicePropertyChanged; static jmethodID method_onInputDeviceConnectionResult; static jmethodID method_onPanDevicePropertyChanged; static jmethodID method_onPanDeviceConnectionResult; typedef event_loop_native_data_t native_data_t; #define EVENT_LOOP_REFS 10 static inline native_data_t * get_native_data(JNIEnv *env, jobject object) { return (native_data_t *)(env->GetIntField(object, field_mNativeData)); } native_data_t *get_EventLoop_native_data(JNIEnv *env, jobject object) { return get_native_data(env, object); } #endif static void classInitNative(JNIEnv* env, jclass clazz) { LOGV(__FUNCTION__); #ifdef HAVE_BLUETOOTH method_onPropertyChanged = env->GetMethodID(clazz, "onPropertyChanged", "([Ljava/lang/String;)V"); method_onDevicePropertyChanged = env->GetMethodID(clazz, "onDevicePropertyChanged", "(Ljava/lang/String;[Ljava/lang/String;)V"); method_onDeviceFound = env->GetMethodID(clazz, "onDeviceFound", "(Ljava/lang/String;[Ljava/lang/String;)V"); method_onDeviceDisappeared = env->GetMethodID(clazz, "onDeviceDisappeared", "(Ljava/lang/String;)V"); method_onDeviceCreated = env->GetMethodID(clazz, "onDeviceCreated", "(Ljava/lang/String;)V"); method_onDeviceRemoved = env->GetMethodID(clazz, "onDeviceRemoved", "(Ljava/lang/String;)V"); method_onDeviceDisconnectRequested = env->GetMethodID(clazz, "onDeviceDisconnectRequested", "(Ljava/lang/String;)V"); method_onNetworkDeviceConnected = env->GetMethodID(clazz, "onNetworkDeviceConnected", "(Ljava/lang/String;Ljava/lang/String;I)V"); method_onNetworkDeviceDisconnected = env->GetMethodID(clazz, "onNetworkDeviceDisconnected", "(Ljava/lang/String;)V"); method_onCreatePairedDeviceResult = env->GetMethodID(clazz, "onCreatePairedDeviceResult", "(Ljava/lang/String;I)V"); method_onCreateDeviceResult = env->GetMethodID(clazz, "onCreateDeviceResult", "(Ljava/lang/String;I)V"); method_onDiscoverServicesResult = env->GetMethodID(clazz, "onDiscoverServicesResult", "(Ljava/lang/String;Z)V"); method_onAgentAuthorize = env->GetMethodID(clazz, "onAgentAuthorize", "(Ljava/lang/String;Ljava/lang/String;)Z"); method_onAgentOutOfBandDataAvailable = env->GetMethodID(clazz, "onAgentOutOfBandDataAvailable", "(Ljava/lang/String;)Z"); method_onAgentCancel = env->GetMethodID(clazz, "onAgentCancel", "()V"); method_onRequestPinCode = env->GetMethodID(clazz, "onRequestPinCode", "(Ljava/lang/String;I)V"); method_onRequestPasskey = env->GetMethodID(clazz, "onRequestPasskey", "(Ljava/lang/String;I)V"); method_onRequestPasskeyConfirmation = env->GetMethodID(clazz, "onRequestPasskeyConfirmation", "(Ljava/lang/String;II)V"); method_onRequestPairingConsent = env->GetMethodID(clazz, "onRequestPairingConsent", "(Ljava/lang/String;I)V"); method_onDisplayPasskey = env->GetMethodID(clazz, "onDisplayPasskey", "(Ljava/lang/String;II)V"); method_onInputDevicePropertyChanged = env->GetMethodID(clazz, "onInputDevicePropertyChanged", "(Ljava/lang/String;[Ljava/lang/String;)V"); method_onInputDeviceConnectionResult = env->GetMethodID(clazz, "onInputDeviceConnectionResult", "(Ljava/lang/String;Z)V"); method_onPanDevicePropertyChanged = env->GetMethodID(clazz, "onPanDevicePropertyChanged", "(Ljava/lang/String;[Ljava/lang/String;)V"); method_onPanDeviceConnectionResult = env->GetMethodID(clazz, "onPanDeviceConnectionResult", "(Ljava/lang/String;Z)V"); method_onRequestOobData = env->GetMethodID(clazz, "onRequestOobData", "(Ljava/lang/String;I)V"); field_mNativeData = env->GetFieldID(clazz, "mNativeData", "I"); #endif } static void initializeNativeDataNative(JNIEnv* env, jobject object) { LOGV(__FUNCTION__); #ifdef HAVE_BLUETOOTH native_data_t *nat = (native_data_t *)calloc(1, sizeof(native_data_t)); if (NULL == nat) { LOGE("%s: out of memory!", __FUNCTION__); return; } memset(nat, 0, sizeof(native_data_t)); pthread_mutex_init(&(nat->thread_mutex), NULL); env->SetIntField(object, field_mNativeData, (jint)nat); { DBusError err; dbus_error_init(&err); dbus_threads_init_default(); nat->conn = dbus_bus_get(DBUS_BUS_SYSTEM, &err); if (dbus_error_is_set(&err)) { LOGE("%s: Could not get onto the system bus!", __FUNCTION__); dbus_error_free(&err); } dbus_connection_set_exit_on_disconnect(nat->conn, FALSE); } #endif } static void cleanupNativeDataNative(JNIEnv* env, jobject object) { LOGV(__FUNCTION__); #ifdef HAVE_BLUETOOTH native_data_t *nat = (native_data_t *)env->GetIntField(object, field_mNativeData); pthread_mutex_destroy(&(nat->thread_mutex)); if (nat) { free(nat); } #endif } #ifdef HAVE_BLUETOOTH static DBusHandlerResult event_filter(DBusConnection *conn, DBusMessage *msg, void *data); DBusHandlerResult agent_event_filter(DBusConnection *conn, DBusMessage *msg, void *data); static int register_agent(native_data_t *nat, const char *agent_path, const char *capabilities); static const DBusObjectPathVTable agent_vtable = { NULL, agent_event_filter, NULL, NULL, NULL, NULL }; static unsigned int unix_events_to_dbus_flags(short events) { return (events & DBUS_WATCH_READABLE ? POLLIN : 0) | (events & DBUS_WATCH_WRITABLE ? POLLOUT : 0) | (events & DBUS_WATCH_ERROR ? POLLERR : 0) | (events & DBUS_WATCH_HANGUP ? POLLHUP : 0); } static short dbus_flags_to_unix_events(unsigned int flags) { return (flags & POLLIN ? DBUS_WATCH_READABLE : 0) | (flags & POLLOUT ? DBUS_WATCH_WRITABLE : 0) | (flags & POLLERR ? DBUS_WATCH_ERROR : 0) | (flags & POLLHUP ? DBUS_WATCH_HANGUP : 0); } static jboolean setUpEventLoop(native_data_t *nat) { LOGV(__FUNCTION__); if (nat != NULL && nat->conn != NULL) { dbus_threads_init_default(); DBusError err; dbus_error_init(&err); // Add a filter for all incoming messages if (!dbus_connection_add_filter(nat->conn, event_filter, nat, NULL)){ return JNI_FALSE; } // Set which messages will be processed by this dbus connection dbus_bus_add_match(nat->conn, "type='signal',interface='org.freedesktop.DBus'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); return JNI_FALSE; } dbus_bus_add_match(nat->conn, "type='signal',interface='"BLUEZ_DBUS_BASE_IFC".Adapter'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); return JNI_FALSE; } dbus_bus_add_match(nat->conn, "type='signal',interface='"BLUEZ_DBUS_BASE_IFC".Device'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); return JNI_FALSE; } dbus_bus_add_match(nat->conn, "type='signal',interface='"BLUEZ_DBUS_BASE_IFC".Input'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); return JNI_FALSE; } dbus_bus_add_match(nat->conn, "type='signal',interface='"BLUEZ_DBUS_BASE_IFC".Network'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); return JNI_FALSE; } dbus_bus_add_match(nat->conn, "type='signal',interface='"BLUEZ_DBUS_BASE_IFC".NetworkServer'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); return JNI_FALSE; } dbus_bus_add_match(nat->conn, "type='signal',interface='org.bluez.AudioSink'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); return JNI_FALSE; } const char *agent_path = "/android/bluetooth/agent"; const char *capabilities = "DisplayYesNo"; if (register_agent(nat, agent_path, capabilities) < 0) { dbus_connection_unregister_object_path (nat->conn, agent_path); return JNI_FALSE; } return JNI_TRUE; } return JNI_FALSE; } const char * get_adapter_path(DBusConnection *conn) { DBusMessage *msg = NULL, *reply = NULL; DBusError err; const char *device_path = NULL; int attempt = 0; for (attempt = 0; attempt < 1000 && reply == NULL; attempt ++) { msg = dbus_message_new_method_call("org.bluez", "/", "org.bluez.Manager", "DefaultAdapter"); if (!msg) { LOGE("%s: Can't allocate new method call for get_adapter_path!", __FUNCTION__); return NULL; } dbus_message_append_args(msg, DBUS_TYPE_INVALID); dbus_error_init(&err); reply = dbus_connection_send_with_reply_and_block(conn, msg, -1, &err); if (!reply) { if (dbus_error_is_set(&err)) { if (dbus_error_has_name(&err, "org.freedesktop.DBus.Error.ServiceUnknown")) { // bluetoothd is still down, retry LOG_AND_FREE_DBUS_ERROR(&err); usleep(10000); // 10 ms continue; } else { // Some other error we weren't expecting LOG_AND_FREE_DBUS_ERROR(&err); } } goto failed; } } if (attempt == 1000) { LOGE("Time out while trying to get Adapter path, is bluetoothd up ?"); goto failed; } if (!dbus_message_get_args(reply, &err, DBUS_TYPE_OBJECT_PATH, &device_path, DBUS_TYPE_INVALID) || !device_path){ if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); } goto failed; } dbus_message_unref(msg); return device_path; failed: dbus_message_unref(msg); return NULL; } static int register_agent(native_data_t *nat, const char * agent_path, const char * capabilities) { DBusMessage *msg, *reply; DBusError err; bool oob = TRUE; if (!dbus_connection_register_object_path(nat->conn, agent_path, &agent_vtable, nat)) { LOGE("%s: Can't register object path %s for agent!", __FUNCTION__, agent_path); return -1; } nat->adapter = get_adapter_path(nat->conn); if (nat->adapter == NULL) { return -1; } msg = dbus_message_new_method_call("org.bluez", nat->adapter, "org.bluez.Adapter", "RegisterAgent"); if (!msg) { LOGE("%s: Can't allocate new method call for agent!", __FUNCTION__); return -1; } dbus_message_append_args(msg, DBUS_TYPE_OBJECT_PATH, &agent_path, DBUS_TYPE_STRING, &capabilities, DBUS_TYPE_BOOLEAN, &oob, DBUS_TYPE_INVALID); dbus_error_init(&err); reply = dbus_connection_send_with_reply_and_block(nat->conn, msg, -1, &err); dbus_message_unref(msg); if (!reply) { LOGE("%s: Can't register agent!", __FUNCTION__); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); } return -1; } dbus_message_unref(reply); dbus_connection_flush(nat->conn); return 0; } static void tearDownEventLoop(native_data_t *nat) { LOGV(__FUNCTION__); if (nat != NULL && nat->conn != NULL) { DBusMessage *msg, *reply; DBusError err; dbus_error_init(&err); const char * agent_path = "/android/bluetooth/agent"; msg = dbus_message_new_method_call("org.bluez", nat->adapter, "org.bluez.Adapter", "UnregisterAgent"); if (msg != NULL) { dbus_message_append_args(msg, DBUS_TYPE_OBJECT_PATH, &agent_path, DBUS_TYPE_INVALID); reply = dbus_connection_send_with_reply_and_block(nat->conn, msg, -1, &err); if (!reply) { if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); dbus_error_free(&err); } } else { dbus_message_unref(reply); } dbus_message_unref(msg); } else { LOGE("%s: Can't create new method call!", __FUNCTION__); } dbus_connection_flush(nat->conn); dbus_connection_unregister_object_path(nat->conn, agent_path); dbus_bus_remove_match(nat->conn, "type='signal',interface='"BLUEZ_DBUS_BASE_IFC".AudioSink'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); } dbus_bus_remove_match(nat->conn, "type='signal',interface='"BLUEZ_DBUS_BASE_IFC".Device'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); } dbus_bus_remove_match(nat->conn, "type='signal',interface='"BLUEZ_DBUS_BASE_IFC".Input'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); } dbus_bus_remove_match(nat->conn, "type='signal',interface='"BLUEZ_DBUS_BASE_IFC".Network'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); } dbus_bus_remove_match(nat->conn, "type='signal',interface='"BLUEZ_DBUS_BASE_IFC".NetworkServer'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); } dbus_bus_remove_match(nat->conn, "type='signal',interface='org.bluez.audio.Manager'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); } dbus_bus_remove_match(nat->conn, "type='signal',interface='"BLUEZ_DBUS_BASE_IFC".Adapter'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); } dbus_bus_remove_match(nat->conn, "type='signal',interface='org.freedesktop.DBus'", &err); if (dbus_error_is_set(&err)) { LOG_AND_FREE_DBUS_ERROR(&err); } dbus_connection_remove_filter(nat->conn, event_filter, nat); } } #define EVENT_LOOP_EXIT 1 #define EVENT_LOOP_ADD 2 #define EVENT_LOOP_REMOVE 3 dbus_bool_t dbusAddWatch(DBusWatch *watch, void *data) { native_data_t *nat = (native_data_t *)data; if (dbus_watch_get_enabled(watch)) { // note that we can't just send the watch and inspect it later // because we may get a removeWatch call before this data is reacted // to by our eventloop and remove this watch.. reading the add first // and then inspecting the recently deceased watch would be bad. char control = EVENT_LOOP_ADD; write(nat->controlFdW, &control, sizeof(char)); int fd = dbus_watch_get_fd(watch); write(nat->controlFdW, &fd, sizeof(int)); unsigned int flags = dbus_watch_get_flags(watch); write(nat->controlFdW, &flags, sizeof(unsigned int)); write(nat->controlFdW, &watch, sizeof(DBusWatch*)); } return true; } void dbusRemoveWatch(DBusWatch *watch, void *data) { native_data_t *nat = (native_data_t *)data; char control = EVENT_LOOP_REMOVE; write(nat->controlFdW, &control, sizeof(char)); int fd = dbus_watch_get_fd(watch); write(nat->controlFdW, &fd, sizeof(int)); unsigned int flags = dbus_watch_get_flags(watch); write(nat->controlFdW, &flags, sizeof(unsigned int)); } void dbusToggleWatch(DBusWatch *watch, void *data) { if (dbus_watch_get_enabled(watch)) { dbusAddWatch(watch, data); } else { dbusRemoveWatch(watch, data); } } static void handleWatchAdd(native_data_t *nat) { DBusWatch *watch; int newFD; unsigned int flags; read(nat->controlFdR, &newFD, sizeof(int)); read(nat->controlFdR, &flags, sizeof(unsigned int)); read(nat->controlFdR, &watch, sizeof(DBusWatch *)); short events = dbus_flags_to_unix_events(flags); for (int y = 0; ypollMemberCount; y++) { if ((nat->pollData[y].fd == newFD) && (nat->pollData[y].events == events)) { LOGV("DBusWatch duplicate add"); return; } } if (nat->pollMemberCount == nat->pollDataSize) { LOGV("Bluetooth EventLoop poll struct growing"); struct pollfd *temp = (struct pollfd *)malloc( sizeof(struct pollfd) * (nat->pollMemberCount+1)); if (!temp) { return; } memcpy(temp, nat->pollData, sizeof(struct pollfd) * nat->pollMemberCount); free(nat->pollData); nat->pollData = temp; DBusWatch **temp2 = (DBusWatch **)malloc(sizeof(DBusWatch *) * (nat->pollMemberCount+1)); if (!temp2) { return; } memcpy(temp2, nat->watchData, sizeof(DBusWatch *) * nat->pollMemberCount); free(nat->watchData); nat->watchData = temp2; nat->pollDataSize++; } nat->pollData[nat->pollMemberCount].fd = newFD; nat->pollData[nat->pollMemberCount].revents = 0; nat->pollData[nat->pollMemberCount].events = events; nat->watchData[nat->pollMemberCount] = watch; nat->pollMemberCount++; } static void handleWatchRemove(native_data_t *nat) { int removeFD; unsigned int flags; read(nat->controlFdR, &removeFD, sizeof(int)); read(nat->controlFdR, &flags, sizeof(unsigned int)); short events = dbus_flags_to_unix_events(flags); for (int y = 0; y < nat->pollMemberCount; y++) { if ((nat->pollData[y].fd == removeFD) && (nat->pollData[y].events == events)) { int newCount = --nat->pollMemberCount; // copy the last live member over this one nat->pollData[y].fd = nat->pollData[newCount].fd; nat->pollData[y].events = nat->pollData[newCount].events; nat->pollData[y].revents = nat->pollData[newCount].revents; nat->watchData[y] = nat->watchData[newCount]; return; } } LOGW("WatchRemove given with unknown watch"); } static void *eventLoopMain(void *ptr) { native_data_t *nat = (native_data_t *)ptr; JNIEnv *env; JavaVMAttachArgs args; char name[] = "BT EventLoop"; args.version = nat->envVer; args.name = name; args.group = NULL; nat->vm->AttachCurrentThread(&env, &args); dbus_connection_set_watch_functions(nat->conn, dbusAddWatch, dbusRemoveWatch, dbusToggleWatch, ptr, NULL); nat->running = true; while (1) { for (int i = 0; i < nat->pollMemberCount; i++) { if (!nat->pollData[i].revents) { continue; } if (nat->pollData[i].fd == nat->controlFdR) { char data; while (recv(nat->controlFdR, &data, sizeof(char), MSG_DONTWAIT) != -1) { switch (data) { case EVENT_LOOP_EXIT: { dbus_connection_set_watch_functions(nat->conn, NULL, NULL, NULL, NULL, NULL); tearDownEventLoop(nat); nat->vm->DetachCurrentThread(); int fd = nat->controlFdR; nat->controlFdR = 0; close(fd); return NULL; } case EVENT_LOOP_ADD: { handleWatchAdd(nat); break; } case EVENT_LOOP_REMOVE: { handleWatchRemove(nat); break; } } } } else { short events = nat->pollData[i].revents; unsigned int flags = unix_events_to_dbus_flags(events); dbus_watch_handle(nat->watchData[i], flags); nat->pollData[i].revents = 0; // can only do one - it may have caused a 'remove' break; } } while (dbus_connection_dispatch(nat->conn) == DBUS_DISPATCH_DATA_REMAINS) { } poll(nat->pollData, nat->pollMemberCount, -1); } } #endif // HAVE_BLUETOOTH static jboolean startEventLoopNative(JNIEnv *env, jobject object) { jboolean result = JNI_FALSE; #ifdef HAVE_BLUETOOTH event_loop_native_data_t *nat = get_native_data(env, object); pthread_mutex_lock(&(nat->thread_mutex)); nat->running = false; if (nat->pollData) { LOGW("trying to start EventLoop a second time!"); pthread_mutex_unlock( &(nat->thread_mutex) ); return JNI_FALSE; } nat->pollData = (struct pollfd *)malloc(sizeof(struct pollfd) * DEFAULT_INITIAL_POLLFD_COUNT); if (!nat->pollData) { LOGE("out of memory error starting EventLoop!"); goto done; } nat->watchData = (DBusWatch **)malloc(sizeof(DBusWatch *) * DEFAULT_INITIAL_POLLFD_COUNT); if (!nat->watchData) { LOGE("out of memory error starting EventLoop!"); goto done; } memset(nat->pollData, 0, sizeof(struct pollfd) * DEFAULT_INITIAL_POLLFD_COUNT); memset(nat->watchData, 0, sizeof(DBusWatch *) * DEFAULT_INITIAL_POLLFD_COUNT); nat->pollDataSize = DEFAULT_INITIAL_POLLFD_COUNT; nat->pollMemberCount = 1; if (socketpair(AF_LOCAL, SOCK_STREAM, 0, &(nat->controlFdR))) { LOGE("Error getting BT control socket"); goto done; } nat->pollData[0].fd = nat->controlFdR; nat->pollData[0].events = POLLIN; env->GetJavaVM( &(nat->vm) ); nat->envVer = env->GetVersion(); nat->me = env->NewGlobalRef(object); if (setUpEventLoop(nat) != JNI_TRUE) { LOGE("failure setting up Event Loop!"); goto done; } pthread_create(&(nat->thread), NULL, eventLoopMain, nat); result = JNI_TRUE; done: if (JNI_FALSE == result) { if (nat->controlFdW) { close(nat->controlFdW); nat->controlFdW = 0; } if (nat->controlFdR) { close(nat->controlFdR); nat->controlFdR = 0; } if (nat->me) env->DeleteGlobalRef(nat->me); nat->me = NULL; if (nat->pollData) free(nat->pollData); nat->pollData = NULL; if (nat->watchData) free(nat->watchData); nat->watchData = NULL; nat->pollDataSize = 0; nat->pollMemberCount = 0; } pthread_mutex_unlock(&(nat->thread_mutex)); #endif // HAVE_BLUETOOTH return result; } static void stopEventLoopNative(JNIEnv *env, jobject object) { #ifdef HAVE_BLUETOOTH native_data_t *nat = get_native_data(env, object); pthread_mutex_lock(&(nat->thread_mutex)); if (nat->pollData) { char data = EVENT_LOOP_EXIT; ssize_t t = write(nat->controlFdW, &data, sizeof(char)); void *ret; pthread_join(nat->thread, &ret); env->DeleteGlobalRef(nat->me); nat->me = NULL; free(nat->pollData); nat->pollData = NULL; free(nat->watchData); nat->watchData = NULL; nat->pollDataSize = 0; nat->pollMemberCount = 0; int fd = nat->controlFdW; nat->controlFdW = 0; close(fd); } nat->running = false; pthread_mutex_unlock(&(nat->thread_mutex)); #endif // HAVE_BLUETOOTH } static jboolean isEventLoopRunningNative(JNIEnv *env, jobject object) { jboolean result = JNI_FALSE; #ifdef HAVE_BLUETOOTH native_data_t *nat = get_native_data(env, object); pthread_mutex_lock(&(nat->thread_mutex)); if (nat->running) { result = JNI_TRUE; } pthread_mutex_unlock(&(nat->thread_mutex)); #endif // HAVE_BLUETOOTH return result; } #ifdef HAVE_BLUETOOTH extern DBusHandlerResult a2dp_event_filter(DBusMessage *msg, JNIEnv *env); // Called by dbus during WaitForAndDispatchEventNative() static DBusHandlerResult event_filter(DBusConnection *conn, DBusMessage *msg, void *data) { native_data_t *nat; JNIEnv *env; DBusError err; DBusHandlerResult ret; dbus_error_init(&err); nat = (native_data_t *)data; nat->vm->GetEnv((void**)&env, nat->envVer); if (dbus_message_get_type(msg) != DBUS_MESSAGE_TYPE_SIGNAL) { LOGV("%s: not interested (not a signal).", __FUNCTION__); return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } LOGE("%s: Received signal %s:%s from %s", __FUNCTION__, dbus_message_get_interface(msg), dbus_message_get_member(msg), dbus_message_get_path(msg)); env->PushLocalFrame(EVENT_LOOP_REFS); if (dbus_message_is_signal(msg, "org.bluez.Adapter", "DeviceFound")) { char *c_address; DBusMessageIter iter; jobjectArray str_array = NULL; if (dbus_message_iter_init(msg, &iter)) { dbus_message_iter_get_basic(&iter, &c_address); if (dbus_message_iter_next(&iter)) str_array = parse_remote_device_properties(env, &iter); } if (str_array != NULL) { env->CallVoidMethod(nat->me, method_onDeviceFound, env->NewStringUTF(c_address), str_array); } else LOG_AND_FREE_DBUS_ERROR_WITH_MSG(&err, msg); goto success; } else if (dbus_message_is_signal(msg, "org.bluez.Adapter", "DeviceDisappeared")) { char *c_address; if (dbus_message_get_args(msg, &err, DBUS_TYPE_STRING, &c_address, DBUS_TYPE_INVALID)) { LOGV("... address = %s", c_address); env->CallVoidMethod(nat->me, method_onDeviceDisappeared, env->NewStringUTF(c_address)); } else LOG_AND_FREE_DBUS_ERROR_WITH_MSG(&err, msg); goto success; } else if (dbus_message_is_signal(msg, "org.bluez.Adapter", "DeviceCreated")) { char *c_object_path; if (dbus_message_get_args(msg, &err, DBUS_TYPE_OBJECT_PATH, &c_object_path, DBUS_TYPE_INVALID)) { LOGV("... address = %s", c_object_path); env->CallVoidMethod(nat->me, method_onDeviceCreated, env->NewStringUTF(c_object_path)); } else LOG_AND_FREE_DBUS_ERROR_WITH_MSG(&err, msg); goto success; } else if (dbus_message_is_signal(msg, "org.bluez.Adapter", "DeviceRemoved")) { char *c_object_path; if (dbus_message_get_args(msg, &err, DBUS_TYPE_OBJECT_PATH, &c_object_path, DBUS_TYPE_INVALID)) { LOGV("... Object Path = %s", c_object_path); env->CallVoidMethod(nat->me, method_onDeviceRemoved, env->NewStringUTF(c_object_path)); } else LOG_AND_FREE_DBUS_ERROR_WITH_MSG(&err, msg); goto success; } else if (dbus_message_is_signal(msg, "org.bluez.Adapter", "PropertyChanged")) { jobjectArray str_array = parse_adapter_property_change(env, msg); if (str_array != NULL) { /* Check if bluetoothd has (re)started, if so update the path. */ jstring property =(jstring) env->GetObjectArrayElement(str_array, 0); const char *c_property = env->GetStringUTFChars(property, NULL); if (!strncmp(c_property, "Powered", strlen("Powered"))) { jstring value = (jstring) env->GetObjectArrayElement(str_array, 1); const char *c_value = env->GetStringUTFChars(value, NULL); if (!strncmp(c_value, "true", strlen("true"))) nat->adapter = get_adapter_path(nat->conn); env->ReleaseStringUTFChars(value, c_value); } env->ReleaseStringUTFChars(property, c_property); env->CallVoidMethod(nat->me, method_onPropertyChanged, str_array); } else LOG_AND_FREE_DBUS_ERROR_WITH_MSG(&err, msg); goto success; } else if (dbus_message_is_signal(msg, "org.bluez.Device", "PropertyChanged")) { jobjectArray str_array = parse_remote_device_property_change(env, msg); if (str_array != NULL) { const char *remote_device_path = dbus_message_get_path(msg); env->CallVoidMethod(nat->me, method_onDevicePropertyChanged, env->NewStringUTF(remote_device_path), str_array); } else LOG_AND_FREE_DBUS_ERROR_WITH_MSG(&err, msg); goto success; } else if (dbus_message_is_signal(msg, "org.bluez.Device", "DisconnectRequested")) { const char *remote_device_path = dbus_message_get_path(msg); env->CallVoidMethod(nat->me, method_onDeviceDisconnectRequested, env->NewStringUTF(remote_device_path)); goto success; } else if (dbus_message_is_signal(msg, "org.bluez.Input", "PropertyChanged")) { jobjectArray str_array = parse_input_property_change(env, msg); if (str_array != NULL) { const char *c_path = dbus_message_get_path(msg); env->CallVoidMethod(nat->me, method_onInputDevicePropertyChanged, env->NewStringUTF(c_path), str_array); } else { LOG_AND_FREE_DBUS_ERROR_WITH_MSG(&err, msg); } goto success; } else if (dbus_message_is_signal(msg, "org.bluez.Network", "PropertyChanged")) { jobjectArray str_array = parse_pan_property_change(env, msg); if (str_array != NULL) { const char *c_path = dbus_message_get_path(msg); env->CallVoidMethod(nat->me, method_onPanDevicePropertyChanged, env->NewStringUTF(c_path), str_array); } else { LOG_AND_FREE_DBUS_ERROR_WITH_MSG(&err, msg); } goto success; } else if (dbus_message_is_signal(msg, "org.bluez.NetworkServer", "DeviceDisconnected")) { char *c_address; if (dbus_message_get_args(msg, &err, DBUS_TYPE_STRING, &c_address, DBUS_TYPE_INVALID)) { env->CallVoidMethod(nat->me, method_onNetworkDeviceDisconnected, env->NewStringUTF(c_address)); } else { LOG_AND_FREE_DBUS_ERROR_WITH_MSG(&err, msg); } goto success; } else if (dbus_message_is_signal(msg, "org.bluez.NetworkServer", "DeviceConnected")) { char *c_address; char *c_iface; uint16_t uuid; if (dbus_message_get_args(msg, &err, DBUS_TYPE_STRING, &c_address, DBUS_TYPE_STRING, &c_iface, DBUS_TYPE_UINT16, &uuid, DBUS_TYPE_INVALID)) { env->CallVoidMethod(nat->me, method_onNetworkDeviceConnected, env->NewStringUTF(c_address), env->NewStringUTF(c_iface), uuid); } else { LOG_AND_FREE_DBUS_ERROR_WITH_MSG(&err, msg); } goto success; } ret = a2dp_event_filter(msg, env); env->PopLocalFrame(NULL); return ret; success: env->PopLocalFrame(NULL); return DBUS_HANDLER_RESULT_HANDLED; } // Called by dbus during WaitForAndDispatchEventNative() DBusHandlerResult agent_event_filter(DBusConnection *conn, DBusMessage *msg, void *data) { native_data_t *nat = (native_data_t *)data; JNIEnv *env; if (dbus_message_get_type(msg) != DBUS_MESSAGE_TYPE_METHOD_CALL) { LOGV("%s: not interested (not a method call).", __FUNCTION__); return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } LOGI("%s: Received method %s:%s", __FUNCTION__, dbus_message_get_interface(msg), dbus_message_get_member(msg)); if (nat == NULL) return DBUS_HANDLER_RESULT_HANDLED; nat->vm->GetEnv((void**)&env, nat->envVer); env->PushLocalFrame(EVENT_LOOP_REFS); if (dbus_message_is_method_call(msg, "org.bluez.Agent", "Cancel")) { env->CallVoidMethod(nat->me, method_onAgentCancel); // reply DBusMessage *reply = dbus_message_new_method_return(msg); if (!reply) { LOGE("%s: Cannot create message reply\n", __FUNCTION__); goto failure; } dbus_connection_send(nat->conn, reply, NULL); dbus_message_unref(reply); goto success; } else if (dbus_message_is_method_call(msg, "org.bluez.Agent", "Authorize")) { char *object_path; const char *uuid; if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &object_path, DBUS_TYPE_STRING, &uuid, DBUS_TYPE_INVALID)) { LOGE("%s: Invalid arguments for Authorize() method", __FUNCTION__); goto failure; } LOGV("... object_path = %s", object_path); LOGV("... uuid = %s", uuid); bool auth_granted = env->CallBooleanMethod(nat->me, method_onAgentAuthorize, env->NewStringUTF(object_path), env->NewStringUTF(uuid)); // reply if (auth_granted) { DBusMessage *reply = dbus_message_new_method_return(msg); if (!reply) { LOGE("%s: Cannot create message reply\n", __FUNCTION__); goto failure; } dbus_connection_send(nat->conn, reply, NULL); dbus_message_unref(reply); } else { DBusMessage *reply = dbus_message_new_error(msg, "org.bluez.Error.Rejected", "Authorization rejected"); if (!reply) { LOGE("%s: Cannot create message reply\n", __FUNCTION__); goto failure; } dbus_connection_send(nat->conn, reply, NULL); dbus_message_unref(reply); } goto success; } else if (dbus_message_is_method_call(msg, "org.bluez.Agent", "OutOfBandAvailable")) { char *object_path; if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &object_path, DBUS_TYPE_INVALID)) { LOGE("%s: Invalid arguments for OutOfBandData available() method", __FUNCTION__); goto failure; } LOGV("... object_path = %s", object_path); bool available = env->CallBooleanMethod(nat->me, method_onAgentOutOfBandDataAvailable, env->NewStringUTF(object_path)); // reply if (available) { DBusMessage *reply = dbus_message_new_method_return(msg); if (!reply) { LOGE("%s: Cannot create message reply\n", __FUNCTION__); goto failure; } dbus_connection_send(nat->conn, reply, NULL); dbus_message_unref(reply); } else { DBusMessage *reply = dbus_message_new_error(msg, "org.bluez.Error.DoesNotExist", "OutofBand data not available"); if (!reply) { LOGE("%s: Cannot create message reply\n", __FUNCTION__); goto failure; } dbus_connection_send(nat->conn, reply, NULL); dbus_message_unref(reply); } goto success; } else if (dbus_message_is_method_call(msg, "org.bluez.Agent", "RequestPinCode")) { char *object_path; if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &object_path, DBUS_TYPE_INVALID)) { LOGE("%s: Invalid arguments for RequestPinCode() method", __FUNCTION__); goto failure; } dbus_message_ref(msg); // increment refcount because we pass to java env->CallVoidMethod(nat->me, method_onRequestPinCode, env->NewStringUTF(object_path), int(msg)); goto success; } else if (dbus_message_is_method_call(msg, "org.bluez.Agent", "RequestPasskey")) { char *object_path; if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &object_path, DBUS_TYPE_INVALID)) { LOGE("%s: Invalid arguments for RequestPasskey() method", __FUNCTION__); goto failure; } dbus_message_ref(msg); // increment refcount because we pass to java env->CallVoidMethod(nat->me, method_onRequestPasskey, env->NewStringUTF(object_path), int(msg)); goto success; } else if (dbus_message_is_method_call(msg, "org.bluez.Agent", "RequestOobData")) { char *object_path; if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &object_path, DBUS_TYPE_INVALID)) { LOGE("%s: Invalid arguments for RequestOobData() method", __FUNCTION__); goto failure; } dbus_message_ref(msg); // increment refcount because we pass to java env->CallVoidMethod(nat->me, method_onRequestOobData, env->NewStringUTF(object_path), int(msg)); goto success; } else if (dbus_message_is_method_call(msg, "org.bluez.Agent", "DisplayPasskey")) { char *object_path; uint32_t passkey; if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &object_path, DBUS_TYPE_UINT32, &passkey, DBUS_TYPE_INVALID)) { LOGE("%s: Invalid arguments for RequestPasskey() method", __FUNCTION__); goto failure; } dbus_message_ref(msg); // increment refcount because we pass to java env->CallVoidMethod(nat->me, method_onDisplayPasskey, env->NewStringUTF(object_path), passkey, int(msg)); goto success; } else if (dbus_message_is_method_call(msg, "org.bluez.Agent", "RequestConfirmation")) { char *object_path; uint32_t passkey; if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &object_path, DBUS_TYPE_UINT32, &passkey, DBUS_TYPE_INVALID)) { LOGE("%s: Invalid arguments for RequestConfirmation() method", __FUNCTION__); goto failure; } dbus_message_ref(msg); // increment refcount because we pass to java env->CallVoidMethod(nat->me, method_onRequestPasskeyConfirmation, env->NewStringUTF(object_path), passkey, int(msg)); goto success; } else if (dbus_message_is_method_call(msg, "org.bluez.Agent", "RequestPairingConsent")) { char *object_path; if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &object_path, DBUS_TYPE_INVALID)) { LOGE("%s: Invalid arguments for RequestPairingConsent() method", __FUNCTION__); goto failure; } dbus_message_ref(msg); // increment refcount because we pass to java env->CallVoidMethod(nat->me, method_onRequestPairingConsent, env->NewStringUTF(object_path), int(msg)); goto success; } else if (dbus_message_is_method_call(msg, "org.bluez.Agent", "Release")) { // reply DBusMessage *reply = dbus_message_new_method_return(msg); if (!reply) { LOGE("%s: Cannot create message reply\n", __FUNCTION__); goto failure; } dbus_connection_send(nat->conn, reply, NULL); dbus_message_unref(reply); goto success; } else { LOGV("%s:%s is ignored", dbus_message_get_interface(msg), dbus_message_get_member(msg)); } failure: env->PopLocalFrame(NULL); return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; success: env->PopLocalFrame(NULL); return DBUS_HANDLER_RESULT_HANDLED; } #endif #ifdef HAVE_BLUETOOTH //TODO: Unify result codes in a header #define BOND_RESULT_ERROR -1000 #define BOND_RESULT_SUCCESS 0 #define BOND_RESULT_AUTH_FAILED 1 #define BOND_RESULT_AUTH_REJECTED 2 #define BOND_RESULT_AUTH_CANCELED 3 #define BOND_RESULT_REMOTE_DEVICE_DOWN 4 #define BOND_RESULT_DISCOVERY_IN_PROGRESS 5 #define BOND_RESULT_AUTH_TIMEOUT 6 #define BOND_RESULT_REPEATED_ATTEMPTS 7 void onCreatePairedDeviceResult(DBusMessage *msg, void *user, void *n) { LOGV(__FUNCTION__); native_data_t *nat = (native_data_t *)n; const char *address = (const char *)user; DBusError err; dbus_error_init(&err); JNIEnv *env; jstring addr; nat->vm->GetEnv((void**)&env, nat->envVer); LOGV("... address = %s", address); jint result = BOND_RESULT_SUCCESS; if (dbus_set_error_from_message(&err, msg)) { if (!strcmp(err.name, BLUEZ_DBUS_BASE_IFC ".Error.AuthenticationFailed")) { // Pins did not match, or remote device did not respond to pin // request in time LOGV("... error = %s (%s)\n", err.name, err.message); result = BOND_RESULT_AUTH_FAILED; } else if (!strcmp(err.name, BLUEZ_DBUS_BASE_IFC ".Error.AuthenticationRejected")) { // We rejected pairing, or the remote side rejected pairing. This // happens if either side presses 'cancel' at the pairing dialog. LOGV("... error = %s (%s)\n", err.name, err.message); result = BOND_RESULT_AUTH_REJECTED; } else if (!strcmp(err.name, BLUEZ_DBUS_BASE_IFC ".Error.AuthenticationCanceled")) { // Not sure if this happens LOGV("... error = %s (%s)\n", err.name, err.message); result = BOND_RESULT_AUTH_CANCELED; } else if (!strcmp(err.name, BLUEZ_DBUS_BASE_IFC ".Error.ConnectionAttemptFailed")) { // Other device is not responding at all LOGV("... error = %s (%s)\n", err.name, err.message); result = BOND_RESULT_REMOTE_DEVICE_DOWN; } else if (!strcmp(err.name, BLUEZ_DBUS_BASE_IFC ".Error.AlreadyExists")) { // already bonded LOGV("... error = %s (%s)\n", err.name, err.message); result = BOND_RESULT_SUCCESS; } else if (!strcmp(err.name, BLUEZ_DBUS_BASE_IFC ".Error.InProgress") && !strcmp(err.message, "Bonding in progress")) { LOGV("... error = %s (%s)\n", err.name, err.message); goto done; } else if (!strcmp(err.name, BLUEZ_DBUS_BASE_IFC ".Error.InProgress") && !strcmp(err.message, "Discover in progress")) { LOGV("... error = %s (%s)\n", err.name, err.message); result = BOND_RESULT_DISCOVERY_IN_PROGRESS; } else if (!strcmp(err.name, BLUEZ_DBUS_BASE_IFC ".Error.RepeatedAttempts")) { LOGV("... error = %s (%s)\n", err.name, err.message); result = BOND_RESULT_REPEATED_ATTEMPTS; } else if (!strcmp(err.name, BLUEZ_DBUS_BASE_IFC ".Error.AuthenticationTimeout")) { LOGV("... error = %s (%s)\n", err.name, err.message); result = BOND_RESULT_AUTH_TIMEOUT; } else { LOGE("%s: D-Bus error: %s (%s)\n", __FUNCTION__, err.name, err.message); result = BOND_RESULT_ERROR; } } addr = env->NewStringUTF(address); env->CallVoidMethod(nat->me, method_onCreatePairedDeviceResult, addr, result); env->DeleteLocalRef(addr); done: dbus_error_free(&err); free(user); } void onCreateDeviceResult(DBusMessage *msg, void *user, void *n) { LOGV(__FUNCTION__); native_data_t *nat = (native_data_t *)n; const char *address= (const char *)user; DBusError err; dbus_error_init(&err); JNIEnv *env; nat->vm->GetEnv((void**)&env, nat->envVer); LOGV("... Address = %s", address); jint result = CREATE_DEVICE_SUCCESS; if (dbus_set_error_from_message(&err, msg)) { if (dbus_error_has_name(&err, "org.bluez.Error.AlreadyExists")) { result = CREATE_DEVICE_ALREADY_EXISTS; } else { result = CREATE_DEVICE_FAILED; } LOG_AND_FREE_DBUS_ERROR(&err); } jstring addr = env->NewStringUTF(address); env->CallVoidMethod(nat->me, method_onCreateDeviceResult, addr, result); env->DeleteLocalRef(addr); free(user); } void onDiscoverServicesResult(DBusMessage *msg, void *user, void *n) { LOGV(__FUNCTION__); native_data_t *nat = (native_data_t *)n; const char *path = (const char *)user; DBusError err; dbus_error_init(&err); JNIEnv *env; nat->vm->GetEnv((void**)&env, nat->envVer); LOGV("... Device Path = %s", path); bool result = JNI_TRUE; if (dbus_set_error_from_message(&err, msg)) { LOG_AND_FREE_DBUS_ERROR(&err); result = JNI_FALSE; } jstring jPath = env->NewStringUTF(path); env->CallVoidMethod(nat->me, method_onDiscoverServicesResult, jPath, result); env->DeleteLocalRef(jPath); free(user); } void onGetDeviceServiceChannelResult(DBusMessage *msg, void *user, void *n) { LOGV(__FUNCTION__); const char *address = (const char *) user; native_data_t *nat = (native_data_t *) n; DBusError err; dbus_error_init(&err); JNIEnv *env; nat->vm->GetEnv((void**)&env, nat->envVer); jint channel = -2; LOGV("... address = %s", address); if (dbus_set_error_from_message(&err, msg) || !dbus_message_get_args(msg, &err, DBUS_TYPE_INT32, &channel, DBUS_TYPE_INVALID)) { LOGE("%s: D-Bus error: %s (%s)\n", __FUNCTION__, err.name, err.message); dbus_error_free(&err); } done: jstring addr = env->NewStringUTF(address); env->CallVoidMethod(nat->me, method_onGetDeviceServiceChannelResult, addr, channel); env->DeleteLocalRef(addr); free(user); } void onInputDeviceConnectionResult(DBusMessage *msg, void *user, void *n) { LOGV(__FUNCTION__); native_data_t *nat = (native_data_t *)n; const char *path = (const char *)user; DBusError err; dbus_error_init(&err); JNIEnv *env; nat->vm->GetEnv((void**)&env, nat->envVer); bool result = JNI_TRUE; if (dbus_set_error_from_message(&err, msg)) { LOG_AND_FREE_DBUS_ERROR(&err); result = JNI_FALSE; } LOGV("... Device Path = %s, result = %d", path, result); jstring jPath = env->NewStringUTF(path); env->CallVoidMethod(nat->me, method_onInputDeviceConnectionResult, jPath, result); env->DeleteLocalRef(jPath); free(user); } void onPanDeviceConnectionResult(DBusMessage *msg, void *user, void *n) { LOGV(__FUNCTION__); native_data_t *nat = (native_data_t *)n; const char *path = (const char *)user; DBusError err; dbus_error_init(&err); JNIEnv *env; nat->vm->GetEnv((void**)&env, nat->envVer); bool result = JNI_TRUE; if (dbus_set_error_from_message(&err, msg)) { LOG_AND_FREE_DBUS_ERROR(&err); result = JNI_FALSE; } LOGV("... Pan Device Path = %s, result = %d", path, result); jstring jPath = env->NewStringUTF(path); env->CallVoidMethod(nat->me, method_onPanDeviceConnectionResult, jPath, result); env->DeleteLocalRef(jPath); free(user); } #endif static JNINativeMethod sMethods[] = { /* name, signature, funcPtr */ {"classInitNative", "()V", (void *)classInitNative}, {"initializeNativeDataNative", "()V", (void *)initializeNativeDataNative}, {"cleanupNativeDataNative", "()V", (void *)cleanupNativeDataNative}, {"startEventLoopNative", "()V", (void *)startEventLoopNative}, {"stopEventLoopNative", "()V", (void *)stopEventLoopNative}, {"isEventLoopRunningNative", "()Z", (void *)isEventLoopRunningNative} }; int register_android_server_BluetoothEventLoop(JNIEnv *env) { return AndroidRuntime::registerNativeMethods(env, "android/server/BluetoothEventLoop", sMethods, NELEM(sMethods)); } } /* namespace android */