| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2004-2009 Marcel Holtmann <marcel@holtmann.org> |
| * |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <stdlib.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <unistd.h> |
| #include <sys/stat.h> |
| #include <sys/ioctl.h> |
| #include <sys/socket.h> |
| |
| #include <bluetooth/bluetooth.h> |
| #include <bluetooth/hci.h> |
| #include <bluetooth/hci_lib.h> |
| #include <bluetooth/hidp.h> |
| #include <bluetooth/l2cap.h> |
| #include <bluetooth/rfcomm.h> |
| #include <bluetooth/sdp.h> |
| #include <bluetooth/sdp_lib.h> |
| |
| #include <glib.h> |
| #include <dbus/dbus.h> |
| #include <gdbus.h> |
| |
| #include "logging.h" |
| #include "textfile.h" |
| #include "uinput.h" |
| |
| #include "../src/storage.h" |
| #include "../src/manager.h" |
| #include "../src/dbus-common.h" |
| #include "adapter.h" |
| #include "../src/device.h" |
| |
| #include "device.h" |
| #include "error.h" |
| #include "fakehid.h" |
| #include "glib-helper.h" |
| #include "btio.h" |
| |
| #define INPUT_DEVICE_INTERFACE "org.bluez.Input" |
| |
| #define BUF_SIZE 16 |
| |
| #define UPDOWN_ENABLED 1 |
| |
| #define FI_FLAG_CONNECTED 1 |
| |
| struct input_conn { |
| struct fake_input *fake; |
| DBusMessage *pending_connect; |
| char *uuid; |
| char *alias; |
| GIOChannel *ctrl_io; |
| GIOChannel *intr_io; |
| guint ctrl_watch; |
| guint intr_watch; |
| int timeout; |
| struct input_device *idev; |
| }; |
| |
| struct input_device { |
| DBusConnection *conn; |
| char *path; |
| bdaddr_t src; |
| bdaddr_t dst; |
| uint32_t handle; |
| guint dc_id; |
| char *name; |
| struct btd_device *device; |
| GSList *connections; |
| }; |
| |
| GSList *devices = NULL; |
| |
| static struct input_device *find_device_by_path(GSList *list, const char *path) |
| { |
| GSList *l; |
| |
| for (l = list; l; l = l->next) { |
| struct input_device *idev = l->data; |
| |
| if (!strcmp(idev->path, path)) |
| return idev; |
| } |
| |
| return NULL; |
| } |
| |
| static struct input_conn *find_connection(GSList *list, const char *pattern) |
| { |
| GSList *l; |
| |
| for (l = list; l; l = l->next) { |
| struct input_conn *iconn = l->data; |
| |
| if (!strcasecmp(iconn->uuid, pattern)) |
| return iconn; |
| |
| if (!strcasecmp(iconn->alias, pattern)) |
| return iconn; |
| } |
| |
| return NULL; |
| } |
| |
| static void input_conn_free(struct input_conn *iconn) |
| { |
| if (iconn->pending_connect) |
| dbus_message_unref(iconn->pending_connect); |
| |
| if (iconn->ctrl_watch) |
| g_source_remove(iconn->ctrl_watch); |
| |
| if (iconn->intr_watch) |
| g_source_remove(iconn->intr_watch); |
| |
| if (iconn->intr_io) |
| g_io_channel_unref(iconn->intr_io); |
| |
| if (iconn->ctrl_io) |
| g_io_channel_unref(iconn->ctrl_io); |
| |
| g_free(iconn->uuid); |
| g_free(iconn->alias); |
| g_free(iconn->fake); |
| g_free(iconn); |
| } |
| |
| static void input_device_free(struct input_device *idev) |
| { |
| if (idev->dc_id) |
| device_remove_disconnect_watch(idev->device, idev->dc_id); |
| |
| dbus_connection_unref(idev->conn); |
| btd_device_unref(idev->device); |
| g_free(idev->name); |
| g_free(idev->path); |
| g_free(idev); |
| } |
| |
| static int uinput_create(char *name) |
| { |
| struct uinput_dev dev; |
| int fd, err; |
| |
| fd = open("/dev/uinput", O_RDWR); |
| if (fd < 0) { |
| fd = open("/dev/input/uinput", O_RDWR); |
| if (fd < 0) { |
| fd = open("/dev/misc/uinput", O_RDWR); |
| if (fd < 0) { |
| err = errno; |
| error("Can't open input device: %s (%d)", |
| strerror(err), err); |
| return -err; |
| } |
| } |
| } |
| |
| memset(&dev, 0, sizeof(dev)); |
| if (name) |
| strncpy(dev.name, name, UINPUT_MAX_NAME_SIZE - 1); |
| |
| dev.id.bustype = BUS_BLUETOOTH; |
| dev.id.vendor = 0x0000; |
| dev.id.product = 0x0000; |
| dev.id.version = 0x0000; |
| |
| if (write(fd, &dev, sizeof(dev)) < 0) { |
| err = errno; |
| error("Can't write device information: %s (%d)", |
| strerror(err), err); |
| close(fd); |
| errno = err; |
| return -err; |
| } |
| |
| ioctl(fd, UI_SET_EVBIT, EV_KEY); |
| ioctl(fd, UI_SET_EVBIT, EV_REL); |
| ioctl(fd, UI_SET_EVBIT, EV_REP); |
| |
| ioctl(fd, UI_SET_KEYBIT, KEY_UP); |
| ioctl(fd, UI_SET_KEYBIT, KEY_PAGEUP); |
| ioctl(fd, UI_SET_KEYBIT, KEY_DOWN); |
| ioctl(fd, UI_SET_KEYBIT, KEY_PAGEDOWN); |
| |
| if (ioctl(fd, UI_DEV_CREATE, NULL) < 0) { |
| err = errno; |
| error("Can't create uinput device: %s (%d)", |
| strerror(err), err); |
| close(fd); |
| errno = err; |
| return -err; |
| } |
| |
| return fd; |
| } |
| |
| static int decode_key(const char *str) |
| { |
| static int mode = UPDOWN_ENABLED, gain = 0; |
| |
| uint16_t key; |
| int new_gain; |
| |
| /* Switch from key up/down to page up/down */ |
| if (strncmp("AT+CKPD=200", str, 11) == 0) { |
| mode = ~mode; |
| return KEY_RESERVED; |
| } |
| |
| if (strncmp("AT+VG", str, 5)) |
| return KEY_RESERVED; |
| |
| /* Gain key pressed */ |
| if (strlen(str) != 10) |
| return KEY_RESERVED; |
| |
| new_gain = strtol(&str[7], NULL, 10); |
| if (new_gain <= gain) |
| key = (mode == UPDOWN_ENABLED ? KEY_UP : KEY_PAGEUP); |
| else |
| key = (mode == UPDOWN_ENABLED ? KEY_DOWN : KEY_PAGEDOWN); |
| |
| gain = new_gain; |
| |
| return key; |
| } |
| |
| static void send_event(int fd, uint16_t type, uint16_t code, int32_t value) |
| { |
| struct uinput_event event; |
| int err; |
| |
| memset(&event, 0, sizeof(event)); |
| event.type = type; |
| event.code = code; |
| event.value = value; |
| |
| err = write(fd, &event, sizeof(event)); |
| } |
| |
| static void send_key(int fd, uint16_t key) |
| { |
| /* Key press */ |
| send_event(fd, EV_KEY, key, 1); |
| send_event(fd, EV_SYN, SYN_REPORT, 0); |
| /* Key release */ |
| send_event(fd, EV_KEY, key, 0); |
| send_event(fd, EV_SYN, SYN_REPORT, 0); |
| } |
| |
| static gboolean rfcomm_io_cb(GIOChannel *chan, GIOCondition cond, gpointer data) |
| { |
| struct fake_input *fake = data; |
| const char *ok = "\r\nOK\r\n"; |
| char buf[BUF_SIZE]; |
| gsize bread = 0, bwritten; |
| uint16_t key; |
| |
| if (cond & G_IO_NVAL) |
| return FALSE; |
| |
| if (cond & (G_IO_HUP | G_IO_ERR)) { |
| error("Hangup or error on rfcomm server socket"); |
| goto failed; |
| } |
| |
| memset(buf, 0, BUF_SIZE); |
| if (g_io_channel_read(chan, buf, sizeof(buf) - 1, |
| &bread) != G_IO_ERROR_NONE) { |
| error("IO Channel read error"); |
| goto failed; |
| } |
| |
| debug("Received: %s", buf); |
| |
| if (g_io_channel_write(chan, ok, 6, &bwritten) != G_IO_ERROR_NONE) { |
| error("IO Channel write error"); |
| goto failed; |
| } |
| |
| key = decode_key(buf); |
| if (key != KEY_RESERVED) |
| send_key(fake->uinput, key); |
| |
| return TRUE; |
| |
| failed: |
| ioctl(fake->uinput, UI_DEV_DESTROY); |
| close(fake->uinput); |
| fake->uinput = -1; |
| g_io_channel_unref(fake->io); |
| |
| return FALSE; |
| } |
| |
| static inline DBusMessage *not_supported(DBusMessage *msg) |
| { |
| return g_dbus_create_error(msg, ERROR_INTERFACE ".Failed", |
| "Not supported"); |
| } |
| |
| static inline DBusMessage *in_progress(DBusMessage *msg) |
| { |
| return g_dbus_create_error(msg, ERROR_INTERFACE ".InProgress", |
| "Device connection already in progress"); |
| } |
| |
| static inline DBusMessage *already_connected(DBusMessage *msg) |
| { |
| return g_dbus_create_error(msg, ERROR_INTERFACE ".AlreadyConnected", |
| "Already connected to a device"); |
| } |
| |
| static inline DBusMessage *connection_attempt_failed(DBusMessage *msg, |
| const char *err) |
| { |
| return g_dbus_create_error(msg, |
| ERROR_INTERFACE ".ConnectionAttemptFailed", |
| err ? err : "Connection attempt failed"); |
| } |
| |
| static void rfcomm_connect_cb(GIOChannel *chan, GError *err, gpointer user_data) |
| { |
| struct input_conn *iconn = user_data; |
| struct input_device *idev = iconn->idev; |
| struct fake_input *fake = iconn->fake; |
| DBusMessage *reply; |
| |
| if (err) { |
| reply = connection_attempt_failed(iconn->pending_connect, |
| err->message); |
| goto failed; |
| } |
| |
| fake->rfcomm = g_io_channel_unix_get_fd(chan); |
| |
| /* |
| * FIXME: Some headsets required a sco connection |
| * first to report volume gain key events |
| */ |
| fake->uinput = uinput_create(idev->name); |
| if (fake->uinput < 0) { |
| g_io_channel_shutdown(chan, TRUE, NULL); |
| reply = connection_attempt_failed(iconn->pending_connect, |
| strerror(errno)); |
| goto failed; |
| } |
| |
| fake->io = g_io_channel_unix_new(fake->rfcomm); |
| g_io_channel_set_close_on_unref(fake->io, TRUE); |
| g_io_add_watch(fake->io, G_IO_IN | G_IO_ERR | G_IO_HUP | G_IO_NVAL, |
| (GIOFunc) rfcomm_io_cb, fake); |
| |
| /* Replying to the requestor */ |
| reply = dbus_message_new_method_return(iconn->pending_connect); |
| g_dbus_send_message(idev->conn, reply); |
| |
| dbus_message_unref(iconn->pending_connect); |
| iconn->pending_connect = NULL; |
| |
| return; |
| |
| failed: |
| g_dbus_send_message(idev->conn, reply); |
| dbus_message_unref(iconn->pending_connect); |
| iconn->pending_connect = NULL; |
| } |
| |
| static gboolean rfcomm_connect(struct input_conn *iconn, GError **err) |
| { |
| struct input_device *idev = iconn->idev; |
| GIOChannel *io; |
| |
| io = bt_io_connect(BT_IO_RFCOMM, rfcomm_connect_cb, iconn, |
| NULL, err, |
| BT_IO_OPT_SOURCE_BDADDR, &idev->src, |
| BT_IO_OPT_DEST_BDADDR, &idev->dst, |
| BT_IO_OPT_INVALID); |
| if (!io) |
| return FALSE; |
| |
| g_io_channel_unref(io); |
| |
| return TRUE; |
| } |
| |
| static gboolean intr_watch_cb(GIOChannel *chan, GIOCondition cond, gpointer data) |
| { |
| struct input_conn *iconn = data; |
| struct input_device *idev = iconn->idev; |
| gboolean connected = FALSE; |
| |
| /* Checking for ctrl_watch avoids a double g_io_channel_shutdown since |
| * it's likely that ctrl_watch_cb has been queued for dispatching in |
| * this mainloop iteration */ |
| if ((cond & (G_IO_HUP | G_IO_ERR)) && iconn->ctrl_watch) |
| g_io_channel_shutdown(chan, TRUE, NULL); |
| |
| emit_property_changed(idev->conn, idev->path, INPUT_DEVICE_INTERFACE, |
| "Connected", DBUS_TYPE_BOOLEAN, &connected); |
| |
| device_remove_disconnect_watch(idev->device, idev->dc_id); |
| idev->dc_id = 0; |
| |
| iconn->intr_watch = 0; |
| |
| g_io_channel_unref(iconn->intr_io); |
| iconn->intr_io = NULL; |
| |
| /* Close control channel */ |
| if (iconn->ctrl_io && !(cond & G_IO_NVAL)) |
| g_io_channel_shutdown(iconn->ctrl_io, TRUE, NULL); |
| |
| return FALSE; |
| } |
| |
| static gboolean ctrl_watch_cb(GIOChannel *chan, GIOCondition cond, gpointer data) |
| { |
| struct input_conn *iconn = data; |
| |
| /* Checking for intr_watch avoids a double g_io_channel_shutdown since |
| * it's likely that intr_watch_cb has been queued for dispatching in |
| * this mainloop iteration */ |
| if ((cond & (G_IO_HUP | G_IO_ERR)) && iconn->intr_watch) |
| g_io_channel_shutdown(chan, TRUE, NULL); |
| |
| iconn->ctrl_watch = 0; |
| |
| g_io_channel_unref(iconn->ctrl_io); |
| iconn->ctrl_io = NULL; |
| |
| /* Close interrupt channel */ |
| if (iconn->intr_io && !(cond & G_IO_NVAL)) |
| g_io_channel_shutdown(iconn->intr_io, TRUE, NULL); |
| |
| return FALSE; |
| } |
| |
| static gboolean fake_hid_connect(struct input_conn *iconn, GError **err) |
| { |
| struct fake_hid *fhid = iconn->fake->priv; |
| |
| return fhid->connect(iconn->fake, err); |
| } |
| |
| static int fake_hid_disconnect(struct input_conn *iconn) |
| { |
| struct fake_hid *fhid = iconn->fake->priv; |
| |
| return fhid->disconnect(iconn->fake); |
| } |
| |
| static void epox_endian_quirk(unsigned char *data, int size) |
| { |
| /* USAGE_PAGE (Keyboard) 05 07 |
| * USAGE_MINIMUM (0) 19 00 |
| * USAGE_MAXIMUM (65280) 2A 00 FF <= must be FF 00 |
| * LOGICAL_MINIMUM (0) 15 00 |
| * LOGICAL_MAXIMUM (65280) 26 00 FF <= must be FF 00 |
| */ |
| unsigned char pattern[] = { 0x05, 0x07, 0x19, 0x00, 0x2a, 0x00, 0xff, |
| 0x15, 0x00, 0x26, 0x00, 0xff }; |
| unsigned int i; |
| |
| if (!data) |
| return; |
| |
| for (i = 0; i < size - sizeof(pattern); i++) { |
| if (!memcmp(data + i, pattern, sizeof(pattern))) { |
| data[i + 5] = 0xff; |
| data[i + 6] = 0x00; |
| data[i + 10] = 0xff; |
| data[i + 11] = 0x00; |
| } |
| } |
| } |
| |
| static void extract_hid_record(sdp_record_t *rec, struct hidp_connadd_req *req) |
| { |
| sdp_data_t *pdlist, *pdlist2; |
| uint8_t attr_val; |
| |
| pdlist = sdp_data_get(rec, 0x0101); |
| pdlist2 = sdp_data_get(rec, 0x0102); |
| if (pdlist) { |
| if (pdlist2) { |
| if (strncmp(pdlist->val.str, pdlist2->val.str, 5)) { |
| strncpy(req->name, pdlist2->val.str, 127); |
| strcat(req->name, " "); |
| } |
| strncat(req->name, pdlist->val.str, 127 - strlen(req->name)); |
| } else |
| strncpy(req->name, pdlist->val.str, 127); |
| } else { |
| pdlist2 = sdp_data_get(rec, 0x0100); |
| if (pdlist2) |
| strncpy(req->name, pdlist2->val.str, 127); |
| } |
| |
| pdlist = sdp_data_get(rec, SDP_ATTR_HID_PARSER_VERSION); |
| req->parser = pdlist ? pdlist->val.uint16 : 0x0100; |
| |
| pdlist = sdp_data_get(rec, SDP_ATTR_HID_DEVICE_SUBCLASS); |
| req->subclass = pdlist ? pdlist->val.uint8 : 0; |
| |
| pdlist = sdp_data_get(rec, SDP_ATTR_HID_COUNTRY_CODE); |
| req->country = pdlist ? pdlist->val.uint8 : 0; |
| |
| pdlist = sdp_data_get(rec, SDP_ATTR_HID_VIRTUAL_CABLE); |
| attr_val = pdlist ? pdlist->val.uint8 : 0; |
| if (attr_val) |
| req->flags |= (1 << HIDP_VIRTUAL_CABLE_UNPLUG); |
| |
| pdlist = sdp_data_get(rec, SDP_ATTR_HID_BOOT_DEVICE); |
| attr_val = pdlist ? pdlist->val.uint8 : 0; |
| if (attr_val) |
| req->flags |= (1 << HIDP_BOOT_PROTOCOL_MODE); |
| |
| pdlist = sdp_data_get(rec, SDP_ATTR_HID_DESCRIPTOR_LIST); |
| if (pdlist) { |
| pdlist = pdlist->val.dataseq; |
| pdlist = pdlist->val.dataseq; |
| pdlist = pdlist->next; |
| |
| req->rd_data = g_try_malloc0(pdlist->unitSize); |
| if (req->rd_data) { |
| memcpy(req->rd_data, (unsigned char *) pdlist->val.str, |
| pdlist->unitSize); |
| req->rd_size = pdlist->unitSize; |
| epox_endian_quirk(req->rd_data, req->rd_size); |
| } |
| } |
| } |
| |
| static int ioctl_connadd(struct hidp_connadd_req *req) |
| { |
| int ctl, err = 0; |
| |
| ctl = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HIDP); |
| if (ctl < 0) |
| return -errno; |
| |
| if (ioctl(ctl, HIDPCONNADD, req) < 0) |
| err = errno; |
| |
| close(ctl); |
| |
| return -err; |
| } |
| |
| static void encrypt_completed(uint8_t status, gpointer user_data) |
| { |
| struct hidp_connadd_req *req = user_data; |
| int err; |
| |
| if (status) { |
| error("Encryption failed: %s(0x%x)", |
| strerror(bt_error(status)), status); |
| goto failed; |
| } |
| |
| err = ioctl_connadd(req); |
| if (err == 0) |
| goto cleanup; |
| |
| error("ioctl_connadd(): %s(%d)", strerror(-err), -err); |
| failed: |
| close(req->intr_sock); |
| close(req->ctrl_sock); |
| |
| cleanup: |
| if (req->rd_data) |
| free(req->rd_data); |
| |
| g_free(req); |
| } |
| |
| static int hidp_add_connection(const struct input_device *idev, |
| const struct input_conn *iconn) |
| { |
| struct hidp_connadd_req *req; |
| struct fake_hid *fake_hid; |
| struct fake_input *fake; |
| sdp_record_t *rec; |
| char src_addr[18], dst_addr[18]; |
| int err; |
| |
| req = g_new0(struct hidp_connadd_req, 1); |
| req->ctrl_sock = g_io_channel_unix_get_fd(iconn->ctrl_io); |
| req->intr_sock = g_io_channel_unix_get_fd(iconn->intr_io); |
| req->flags = 0; |
| req->idle_to = iconn->timeout; |
| |
| ba2str(&idev->src, src_addr); |
| ba2str(&idev->dst, dst_addr); |
| |
| rec = fetch_record(src_addr, dst_addr, idev->handle); |
| if (!rec) { |
| error("Rejected connection from unknown device %s", dst_addr); |
| err = -EPERM; |
| goto cleanup; |
| } |
| |
| extract_hid_record(rec, req); |
| sdp_record_free(rec); |
| |
| read_device_id(src_addr, dst_addr, NULL, |
| &req->vendor, &req->product, &req->version); |
| |
| fake_hid = get_fake_hid(req->vendor, req->product); |
| if (fake_hid) { |
| fake = g_new0(struct fake_input, 1); |
| fake->connect = fake_hid_connect; |
| fake->disconnect = fake_hid_disconnect; |
| fake->priv = fake_hid; |
| err = fake_hid_connadd(fake, iconn->intr_io, fake_hid); |
| goto cleanup; |
| } |
| |
| if (idev->name) |
| strncpy(req->name, idev->name, sizeof(req->name) - 1); |
| |
| /* Encryption is mandatory for keyboards */ |
| if (req->subclass & 0x40) { |
| err = bt_acl_encrypt(&idev->src, &idev->dst, encrypt_completed, req); |
| if (err == 0) { |
| /* Waiting async encryption */ |
| return 0; |
| } else if (err != -EALREADY) { |
| error("bt_acl_encrypt(): %s(%d)", strerror(-err), -err); |
| goto cleanup; |
| } |
| } |
| |
| if (req->vendor == 0x054c && req->product == 0x0268) { |
| unsigned char buf[] = { 0x53, 0xf4, 0x42, 0x03, 0x00, 0x00 }; |
| int sk = g_io_channel_unix_get_fd(iconn->ctrl_io); |
| err = write(sk, buf, sizeof(buf)); |
| } |
| |
| err = ioctl_connadd(req); |
| |
| cleanup: |
| if (req->rd_data) |
| free(req->rd_data); |
| g_free(req); |
| |
| return err; |
| } |
| |
| static int is_connected(struct input_conn *iconn) |
| { |
| struct input_device *idev = iconn->idev; |
| struct fake_input *fake = iconn->fake; |
| struct hidp_conninfo ci; |
| int ctl; |
| |
| /* Fake input */ |
| if (fake) |
| return fake->flags & FI_FLAG_CONNECTED; |
| |
| /* Standard HID */ |
| ctl = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HIDP); |
| if (ctl < 0) |
| return 0; |
| |
| memset(&ci, 0, sizeof(ci)); |
| bacpy(&ci.bdaddr, &idev->dst); |
| if (ioctl(ctl, HIDPGETCONNINFO, &ci) < 0) { |
| close(ctl); |
| return 0; |
| } |
| |
| close(ctl); |
| |
| if (ci.state != BT_CONNECTED) |
| return 0; |
| else |
| return 1; |
| } |
| |
| static int connection_disconnect(struct input_conn *iconn, uint32_t flags) |
| { |
| struct input_device *idev = iconn->idev; |
| struct fake_input *fake = iconn->fake; |
| struct hidp_conndel_req req; |
| struct hidp_conninfo ci; |
| int ctl, err; |
| |
| /* Fake input disconnect */ |
| if (fake) { |
| err = fake->disconnect(iconn); |
| if (err == 0) |
| fake->flags &= ~FI_FLAG_CONNECTED; |
| return err; |
| } |
| |
| /* Standard HID disconnect */ |
| if (iconn->intr_io) |
| g_io_channel_shutdown(iconn->intr_io, TRUE, NULL); |
| if (iconn->ctrl_io) |
| g_io_channel_shutdown(iconn->ctrl_io, TRUE, NULL); |
| |
| ctl = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HIDP); |
| if (ctl < 0) { |
| error("Can't open HIDP control socket"); |
| return -errno; |
| } |
| |
| memset(&ci, 0, sizeof(ci)); |
| bacpy(&ci.bdaddr, &idev->dst); |
| if ((ioctl(ctl, HIDPGETCONNINFO, &ci) < 0) || |
| (ci.state != BT_CONNECTED)) { |
| errno = ENOTCONN; |
| goto fail; |
| } |
| |
| memset(&req, 0, sizeof(req)); |
| bacpy(&req.bdaddr, &idev->dst); |
| req.flags = flags; |
| if (ioctl(ctl, HIDPCONNDEL, &req) < 0) { |
| error("Can't delete the HID device: %s(%d)", |
| strerror(errno), errno); |
| goto fail; |
| } |
| |
| close(ctl); |
| |
| return 0; |
| |
| fail: |
| err = errno; |
| close(ctl); |
| errno = err; |
| |
| return -err; |
| } |
| |
| static int disconnect(struct input_device *idev, uint32_t flags) |
| { |
| struct input_conn *iconn = NULL; |
| GSList *l; |
| |
| for (l = idev->connections; l; l = l->next) { |
| iconn = l->data; |
| |
| if (is_connected(iconn)) |
| break; |
| } |
| |
| if (!iconn) |
| return ENOTCONN; |
| |
| return connection_disconnect(iconn, flags); |
| } |
| |
| static void disconnect_cb(struct btd_device *device, gboolean removal, |
| void *user_data) |
| { |
| struct input_device *idev = user_data; |
| int flags; |
| |
| info("Input: disconnect %s", idev->path); |
| |
| flags = removal ? (1 << HIDP_VIRTUAL_CABLE_UNPLUG) : 0; |
| |
| disconnect(idev, flags); |
| } |
| |
| static int input_device_connected(struct input_device *idev, |
| struct input_conn *iconn) |
| { |
| dbus_bool_t connected; |
| int err; |
| |
| if (iconn->intr_io == NULL || iconn->ctrl_io == NULL) |
| return -ENOTCONN; |
| |
| err = hidp_add_connection(idev, iconn); |
| if (err < 0) |
| return err; |
| |
| iconn->intr_watch = g_io_add_watch(iconn->intr_io, |
| G_IO_HUP | G_IO_ERR | G_IO_NVAL, |
| intr_watch_cb, iconn); |
| iconn->ctrl_watch = g_io_add_watch(iconn->ctrl_io, |
| G_IO_HUP | G_IO_ERR | G_IO_NVAL, |
| ctrl_watch_cb, iconn); |
| |
| connected = TRUE; |
| emit_property_changed(idev->conn, idev->path, INPUT_DEVICE_INTERFACE, |
| "Connected", DBUS_TYPE_BOOLEAN, &connected); |
| |
| idev->dc_id = device_add_disconnect_watch(idev->device, disconnect_cb, |
| idev, NULL); |
| |
| return 0; |
| } |
| |
| static void interrupt_connect_cb(GIOChannel *chan, GError *conn_err, |
| gpointer user_data) |
| { |
| struct input_conn *iconn = user_data; |
| struct input_device *idev = iconn->idev; |
| DBusMessage *reply; |
| int err; |
| const char *err_msg; |
| |
| if (conn_err) { |
| err_msg = conn_err->message; |
| g_io_channel_unref(iconn->intr_io); |
| iconn->intr_io = NULL; |
| goto failed; |
| } |
| |
| err = input_device_connected(idev, iconn); |
| if (err < 0) { |
| err_msg = strerror(-err); |
| goto failed; |
| } |
| |
| /* Replying to the requestor */ |
| g_dbus_send_reply(idev->conn, iconn->pending_connect, DBUS_TYPE_INVALID); |
| |
| dbus_message_unref(iconn->pending_connect); |
| iconn->pending_connect = NULL; |
| |
| return; |
| |
| failed: |
| error("%s", err_msg); |
| reply = connection_attempt_failed(iconn->pending_connect, err_msg); |
| g_dbus_send_message(idev->conn, reply); |
| |
| if (iconn->ctrl_io) |
| g_io_channel_shutdown(iconn->ctrl_io, FALSE, NULL); |
| |
| if (iconn->intr_io) { |
| if (!conn_err) |
| g_io_channel_shutdown(iconn->intr_io, FALSE, NULL); |
| g_io_channel_unref(iconn->intr_io); |
| iconn->intr_io = NULL; |
| } |
| } |
| |
| static void control_connect_cb(GIOChannel *chan, GError *conn_err, |
| gpointer user_data) |
| { |
| struct input_conn *iconn = user_data; |
| struct input_device *idev = iconn->idev; |
| DBusMessage *reply; |
| GIOChannel *io; |
| GError *err = NULL; |
| |
| if (conn_err) { |
| error("%s", conn_err->message); |
| reply = connection_attempt_failed(iconn->pending_connect, |
| conn_err->message); |
| goto failed; |
| } |
| |
| /* Connect to the HID interrupt channel */ |
| io = bt_io_connect(BT_IO_L2CAP, interrupt_connect_cb, iconn, |
| NULL, &err, |
| BT_IO_OPT_SOURCE_BDADDR, &idev->src, |
| BT_IO_OPT_DEST_BDADDR, &idev->dst, |
| BT_IO_OPT_PSM, L2CAP_PSM_HIDP_INTR, |
| BT_IO_OPT_INVALID); |
| if (!io) { |
| error("%s", err->message); |
| reply = connection_attempt_failed(iconn->pending_connect, |
| err->message); |
| g_error_free(err); |
| g_io_channel_shutdown(chan, TRUE, NULL); |
| goto failed; |
| } |
| |
| iconn->intr_io = io; |
| |
| return; |
| |
| failed: |
| g_dbus_send_message(idev->conn, reply); |
| dbus_message_unref(iconn->pending_connect); |
| iconn->pending_connect = NULL; |
| } |
| |
| static int fake_disconnect(struct input_conn *iconn) |
| { |
| struct fake_input *fake = iconn->fake; |
| |
| if (!fake->io) |
| return -ENOTCONN; |
| |
| g_io_channel_shutdown(fake->io, TRUE, NULL); |
| g_io_channel_unref(fake->io); |
| fake->io = NULL; |
| |
| if (fake->uinput >= 0) { |
| ioctl(fake->uinput, UI_DEV_DESTROY); |
| close(fake->uinput); |
| fake->uinput = -1; |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * Input Device methods |
| */ |
| static DBusMessage *input_device_connect(DBusConnection *conn, |
| DBusMessage *msg, void *data) |
| { |
| struct input_device *idev = data; |
| struct input_conn *iconn; |
| struct fake_input *fake; |
| DBusMessage *reply; |
| GError *err = NULL; |
| |
| iconn = find_connection(idev->connections, "HID"); |
| if (!iconn) |
| return not_supported(msg); |
| |
| if (iconn->pending_connect) |
| return in_progress(msg); |
| |
| if (is_connected(iconn)) |
| return already_connected(msg); |
| |
| iconn->pending_connect = dbus_message_ref(msg); |
| fake = iconn->fake; |
| |
| if (fake) { |
| /* Fake input device */ |
| if (fake->connect(iconn, &err)) |
| fake->flags |= FI_FLAG_CONNECTED; |
| } else { |
| /* HID devices */ |
| GIOChannel *io; |
| |
| io = bt_io_connect(BT_IO_L2CAP, control_connect_cb, iconn, |
| NULL, &err, |
| BT_IO_OPT_SOURCE_BDADDR, &idev->src, |
| BT_IO_OPT_DEST_BDADDR, &idev->dst, |
| BT_IO_OPT_PSM, L2CAP_PSM_HIDP_CTRL, |
| BT_IO_OPT_INVALID); |
| iconn->ctrl_io = io; |
| } |
| |
| if (err == NULL) |
| return NULL; |
| |
| error("%s", err->message); |
| dbus_message_unref(iconn->pending_connect); |
| iconn->pending_connect = NULL; |
| reply = connection_attempt_failed(msg, err->message); |
| g_error_free(err); |
| return reply; |
| } |
| |
| static DBusMessage *create_errno_message(DBusMessage *msg, int err) |
| { |
| return g_dbus_create_error(msg, ERROR_INTERFACE ".Failed", |
| strerror(err)); |
| } |
| |
| static DBusMessage *input_device_disconnect(DBusConnection *conn, |
| DBusMessage *msg, void *data) |
| { |
| struct input_device *idev = data; |
| int err; |
| |
| err = disconnect(idev, 0); |
| if (err < 0) |
| return create_errno_message(msg, -err); |
| |
| return g_dbus_create_reply(msg, DBUS_TYPE_INVALID); |
| } |
| |
| static void device_unregister(void *data) |
| { |
| struct input_device *idev = data; |
| |
| debug("Unregistered interface %s on path %s", INPUT_DEVICE_INTERFACE, |
| idev->path); |
| |
| devices = g_slist_remove(devices, idev); |
| input_device_free(idev); |
| } |
| |
| static gint connected_cmp(gpointer a, gpointer b) |
| { |
| struct input_conn *iconn = a; |
| |
| return !is_connected(iconn); |
| } |
| |
| static DBusMessage *input_device_get_properties(DBusConnection *conn, |
| DBusMessage *msg, void *data) |
| { |
| struct input_device *idev = data; |
| DBusMessage *reply; |
| DBusMessageIter iter; |
| DBusMessageIter dict; |
| dbus_bool_t connected; |
| |
| reply = dbus_message_new_method_return(msg); |
| if (!reply) |
| return NULL; |
| |
| dbus_message_iter_init_append(reply, &iter); |
| |
| dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, |
| DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING |
| DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING |
| DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict); |
| |
| /* Connected */ |
| connected = !!g_slist_find_custom(idev->connections, NULL, |
| (GCompareFunc) connected_cmp); |
| dict_append_entry(&dict, "Connected", DBUS_TYPE_BOOLEAN, &connected); |
| |
| dbus_message_iter_close_container(&iter, &dict); |
| |
| return reply; |
| } |
| |
| static GDBusMethodTable device_methods[] = { |
| { "Connect", "", "", input_device_connect, |
| G_DBUS_METHOD_FLAG_ASYNC }, |
| { "Disconnect", "", "", input_device_disconnect }, |
| { "GetProperties", "", "a{sv}",input_device_get_properties }, |
| { } |
| }; |
| |
| static GDBusSignalTable device_signals[] = { |
| { "PropertyChanged", "sv" }, |
| { } |
| }; |
| |
| static struct input_device *input_device_new(DBusConnection *conn, |
| struct btd_device *device, const char *path, |
| const bdaddr_t *src, const bdaddr_t *dst, |
| const uint32_t handle) |
| { |
| struct input_device *idev; |
| char name[249], src_addr[18], dst_addr[18]; |
| |
| idev = g_new0(struct input_device, 1); |
| bacpy(&idev->src, src); |
| bacpy(&idev->dst, dst); |
| idev->device = btd_device_ref(device); |
| idev->path = g_strdup(path); |
| idev->conn = dbus_connection_ref(conn); |
| idev->handle = handle; |
| |
| ba2str(src, src_addr); |
| ba2str(dst, dst_addr); |
| if (read_device_name(src_addr, dst_addr, name) == 0) |
| idev->name = g_strdup(name); |
| |
| if (g_dbus_register_interface(conn, idev->path, INPUT_DEVICE_INTERFACE, |
| device_methods, device_signals, NULL, |
| idev, device_unregister) == FALSE) { |
| error("Failed to register interface %s on path %s", |
| INPUT_DEVICE_INTERFACE, path); |
| input_device_free(idev); |
| return NULL; |
| } |
| |
| debug("Registered interface %s on path %s", |
| INPUT_DEVICE_INTERFACE, idev->path); |
| |
| return idev; |
| } |
| |
| static struct input_conn *input_conn_new(struct input_device *idev, |
| const char *uuid, const char *alias, |
| int timeout) |
| { |
| struct input_conn *iconn; |
| |
| iconn = g_new0(struct input_conn, 1); |
| iconn->timeout = timeout; |
| iconn->uuid = g_strdup(uuid); |
| iconn->alias = g_strdup(alias); |
| iconn->idev = idev; |
| |
| return iconn; |
| } |
| |
| int input_device_register(DBusConnection *conn, struct btd_device *device, |
| const char *path, const bdaddr_t *src, |
| const bdaddr_t *dst, const char *uuid, |
| uint32_t handle, int timeout) |
| { |
| struct input_device *idev; |
| struct input_conn *iconn; |
| |
| idev = find_device_by_path(devices, path); |
| if (!idev) { |
| idev = input_device_new(conn, device, path, src, dst, handle); |
| if (!idev) |
| return -EINVAL; |
| devices = g_slist_append(devices, idev); |
| } |
| |
| iconn = input_conn_new(idev, uuid, "hid", timeout); |
| if (!iconn) |
| return -EINVAL; |
| |
| idev->connections = g_slist_append(idev->connections, iconn); |
| |
| return 0; |
| } |
| |
| int fake_input_register(DBusConnection *conn, struct btd_device *device, |
| const char *path, bdaddr_t *src, bdaddr_t *dst, |
| const char *uuid, uint8_t channel) |
| { |
| struct input_device *idev; |
| struct input_conn *iconn; |
| |
| idev = find_device_by_path(devices, path); |
| if (!idev) { |
| idev = input_device_new(conn, device, path, src, dst, 0); |
| if (!idev) |
| return -EINVAL; |
| devices = g_slist_append(devices, idev); |
| } |
| |
| iconn = input_conn_new(idev, uuid, "hsp", 0); |
| if (!iconn) |
| return -EINVAL; |
| |
| iconn->fake = g_new0(struct fake_input, 1); |
| iconn->fake->ch = channel; |
| iconn->fake->connect = rfcomm_connect; |
| iconn->fake->disconnect = fake_disconnect; |
| |
| idev->connections = g_slist_append(idev->connections, iconn); |
| |
| return 0; |
| } |
| |
| static struct input_device *find_device(const bdaddr_t *src, |
| const bdaddr_t *dst) |
| { |
| GSList *list; |
| |
| for (list = devices; list != NULL; list = list->next) { |
| struct input_device *idev = list->data; |
| |
| if (!bacmp(&idev->src, src) && !bacmp(&idev->dst, dst)) |
| return idev; |
| } |
| |
| return NULL; |
| } |
| |
| int input_device_unregister(const char *path, const char *uuid) |
| { |
| struct input_device *idev; |
| struct input_conn *iconn; |
| |
| idev = find_device_by_path(devices, path); |
| if (idev == NULL) |
| return -EINVAL; |
| |
| iconn = find_connection(idev->connections, uuid); |
| if (iconn == NULL) |
| return -EINVAL; |
| |
| if (iconn->pending_connect) { |
| /* Pending connection running */ |
| return -EBUSY; |
| } |
| |
| idev->connections = g_slist_remove(idev->connections, iconn); |
| input_conn_free(iconn); |
| if (idev->connections) |
| return 0; |
| |
| g_dbus_unregister_interface(idev->conn, path, INPUT_DEVICE_INTERFACE); |
| |
| return 0; |
| } |
| |
| static int input_device_connadd(struct input_device *idev, |
| struct input_conn *iconn) |
| { |
| int err; |
| |
| err = input_device_connected(idev, iconn); |
| if (err < 0) |
| goto error; |
| |
| return 0; |
| |
| error: |
| if (iconn->ctrl_io) { |
| g_io_channel_shutdown(iconn->ctrl_io, FALSE, NULL); |
| g_io_channel_unref(iconn->ctrl_io); |
| iconn->ctrl_io = NULL; |
| } |
| if (iconn->intr_io) { |
| g_io_channel_shutdown(iconn->intr_io, FALSE, NULL); |
| g_io_channel_unref(iconn->intr_io); |
| iconn->intr_io = NULL; |
| } |
| |
| return err; |
| } |
| |
| int input_device_set_channel(const bdaddr_t *src, const bdaddr_t *dst, int psm, |
| GIOChannel *io) |
| { |
| struct input_device *idev = find_device(src, dst); |
| struct input_conn *iconn; |
| |
| if (!idev) |
| return -ENOENT; |
| |
| iconn = find_connection(idev->connections, "hid"); |
| if (!iconn) |
| return -ENOENT; |
| |
| switch (psm) { |
| case L2CAP_PSM_HIDP_CTRL: |
| if (iconn->ctrl_io) |
| return -EALREADY; |
| iconn->ctrl_io = g_io_channel_ref(io); |
| break; |
| case L2CAP_PSM_HIDP_INTR: |
| if (iconn->intr_io) |
| return -EALREADY; |
| iconn->intr_io = g_io_channel_ref(io); |
| break; |
| } |
| |
| if (iconn->intr_io && iconn->ctrl_io) |
| input_device_connadd(idev, iconn); |
| |
| return 0; |
| } |
| |
| int input_device_close_channels(const bdaddr_t *src, const bdaddr_t *dst) |
| { |
| struct input_device *idev = find_device(src, dst); |
| struct input_conn *iconn; |
| |
| if (!idev) |
| return -ENOENT; |
| |
| iconn = find_connection(idev->connections, "hid"); |
| if (!iconn) |
| return -ENOENT; |
| |
| if (iconn->intr_io) |
| g_io_channel_shutdown(iconn->intr_io, TRUE, NULL); |
| |
| if (iconn->ctrl_io) |
| g_io_channel_shutdown(iconn->ctrl_io, TRUE, NULL); |
| |
| return 0; |
| } |