GamepadsGtk.cpp   [plain text]


/*
 * Copyright (C) 2012 Zan Dobersek <zandobersek@gmail.com>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 */

#include "config.h"
#include "Gamepads.h"

#if ENABLE(GAMEPAD)

#include "GamepadDeviceLinux.h"
#include "GamepadList.h"
#include "Logging.h"
#include <gio/gunixinputstream.h>
#include <gudev/gudev.h>
#include <wtf/HashMap.h>
#include <wtf/PassOwnPtr.h>
#include <wtf/gobject/GOwnPtr.h>
#include <wtf/gobject/GRefPtr.h>
#include <wtf/text/CString.h>
#include <wtf/text/StringHash.h>

namespace WebCore {

class GamepadDeviceGtk : public GamepadDeviceLinux {
public:
    static PassOwnPtr<GamepadDeviceGtk> create(String deviceFile)
    {
        return adoptPtr(new GamepadDeviceGtk(deviceFile));
    }
    ~GamepadDeviceGtk();

private:
    GamepadDeviceGtk(String deviceFile);

    static gboolean readCallback(GObject* pollableStream, gpointer data);
    GRefPtr<GInputStream> m_inputStream;
    GRefPtr<GSource> m_source;
};

GamepadDeviceGtk::GamepadDeviceGtk(String deviceFile)
    : GamepadDeviceLinux(deviceFile)
{
    if (m_fileDescriptor == -1)
        return;

    m_inputStream = adoptGRef(g_unix_input_stream_new(m_fileDescriptor, FALSE));
    m_source = adoptGRef(g_pollable_input_stream_create_source(G_POLLABLE_INPUT_STREAM(m_inputStream.get()), 0));
    g_source_set_callback(m_source.get(), reinterpret_cast<GSourceFunc>(readCallback), this, 0);
    g_source_attach(m_source.get(), 0);
}

GamepadDeviceGtk::~GamepadDeviceGtk()
{
    if (m_source)
        g_source_destroy(m_source.get());
}

gboolean GamepadDeviceGtk::readCallback(GObject* pollableStream, gpointer data)
{
    GamepadDeviceGtk* gamepadDevice = reinterpret_cast<GamepadDeviceGtk*>(data);
    GOwnPtr<GError> error;
    struct js_event event;

    gssize len = g_pollable_input_stream_read_nonblocking(G_POLLABLE_INPUT_STREAM(pollableStream),
                                                          &event, sizeof(event), 0, &error.outPtr());

    // FIXME: Properly log the error.
    // In the case of G_IO_ERROR_WOULD_BLOCK error return TRUE to wait until
    // the source becomes readable again and FALSE otherwise.
    if (error)
        return g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK);

    ASSERT_UNUSED(len, len == sizeof(event));
    gamepadDevice->updateForEvent(event);
    return TRUE;
}

class GamepadsGtk {
public:
    GamepadsGtk(unsigned length);

    void registerDevice(String deviceFile);
    void unregisterDevice(String deviceFile);

    void updateGamepadList(GamepadList*);

private:
    ~GamepadsGtk();
    static void onUEventCallback(GUdevClient*, gchar* action, GUdevDevice*, gpointer data);
    static gboolean isGamepadDevice(GUdevDevice*);

    Vector<OwnPtr<GamepadDeviceGtk> > m_slots;
    HashMap<String, GamepadDeviceGtk*> m_deviceMap;

    GRefPtr<GUdevClient> m_gudevClient;
};

GamepadsGtk::GamepadsGtk(unsigned length)
    : m_slots(length)
{
    static const char* subsystems[] = { "input", 0 };
    m_gudevClient = adoptGRef(g_udev_client_new(subsystems));
    g_signal_connect(m_gudevClient.get(), "uevent", G_CALLBACK(onUEventCallback), this);

    GOwnPtr<GList> devicesList(g_udev_client_query_by_subsystem(m_gudevClient.get(), subsystems[0]));
    for (GList* listItem = devicesList.get(); listItem; listItem = g_list_next(listItem)) {
        GUdevDevice* device = G_UDEV_DEVICE(listItem->data);
        String deviceFile = String::fromUTF8(g_udev_device_get_device_file(device));
        if (isGamepadDevice(device))
            registerDevice(deviceFile);
        g_object_unref(device);
    }
}

GamepadsGtk::~GamepadsGtk()
{
    g_signal_handlers_disconnect_matched(m_gudevClient.get(), G_SIGNAL_MATCH_DATA, 0, 0, 0, 0, this);
}

void GamepadsGtk::registerDevice(String deviceFile)
{
    LOG(Gamepad, "GamepadsGtk::registerDevice %s", deviceFile.ascii().data());
    ASSERT(!m_deviceMap.contains(deviceFile));

    for (unsigned index = 0; index < m_slots.size(); index++) {
        if (!m_slots[index]) {
            m_slots[index] = GamepadDeviceGtk::create(deviceFile);
            m_deviceMap.add(deviceFile, m_slots[index].get());
            break;
        }
    }
}

void GamepadsGtk::unregisterDevice(String deviceFile)
{
    LOG(Gamepad, "GamepadsGtk::unregisterDevice %s", deviceFile.ascii().data());
    ASSERT(m_deviceMap.contains(deviceFile));

    GamepadDeviceGtk* gamepadDevice = m_deviceMap.take(deviceFile);
    size_t index = m_slots.find(gamepadDevice);
    ASSERT(index != notFound);

    m_slots[index].clear();
}

void GamepadsGtk::updateGamepadList(GamepadList* into)
{
    ASSERT(m_slots.size() == into->length());

    for (unsigned i = 0; i < m_slots.size(); i++) {
        if (m_slots[i].get() && m_slots[i]->connected()) {
            GamepadDeviceGtk* gamepadDevice = m_slots[i].get();
            RefPtr<Gamepad> gamepad = into->item(i);
            if (!gamepad)
                gamepad = Gamepad::create();

            gamepad->index(i);
            gamepad->id(gamepadDevice->id());
            gamepad->timestamp(gamepadDevice->timestamp());
            gamepad->axes(gamepadDevice->axesCount(), gamepadDevice->axesData());
            gamepad->buttons(gamepadDevice->buttonsCount(), gamepadDevice->buttonsData());

            into->set(i, gamepad);
        } else
            into->set(i, 0);
    }
}

void GamepadsGtk::onUEventCallback(GUdevClient* udevClient, gchar* action, GUdevDevice* device, gpointer data)
{
    if (!isGamepadDevice(device))
        return;

    GamepadsGtk* gamepadsGtk = reinterpret_cast<GamepadsGtk*>(data);
    String deviceFile = String::fromUTF8(g_udev_device_get_device_file(device));

    if (!g_strcmp0(action, "add"))
        gamepadsGtk->registerDevice(deviceFile);
    else if (!g_strcmp0(action, "remove"))
        gamepadsGtk->unregisterDevice(deviceFile);
}

gboolean GamepadsGtk::isGamepadDevice(GUdevDevice* device)
{
    const gchar* deviceFile = g_udev_device_get_device_file(device);
    const gchar* sysfsPath = g_udev_device_get_sysfs_path(device);
    if (!deviceFile || !sysfsPath)
        return FALSE;

    if (!g_udev_device_has_property(device, "ID_INPUT") || !g_udev_device_has_property(device, "ID_INPUT_JOYSTICK"))
        return FALSE;

    return g_str_has_prefix(deviceFile, "/dev/input/js");
}

void sampleGamepads(GamepadList* into)
{
    DEFINE_STATIC_LOCAL(GamepadsGtk, gamepadsGtk, (into->length()));
    gamepadsGtk.updateGamepadList(into);
}

} // namespace WebCore

#endif // ENABLE(GAMEPAD)