bug 852935 - Add Android gamepad backend. r=snorp, rs=smaug
authorTed Mielczarek <ted@mielczarek.org>
Tue, 22 Apr 2014 10:53:48 -0400
changeset 181435 885b5b8fd7d95a83a98151b5d68d65fc2a164980
parent 181434 74ef5120ae2f590d8b766806025610f200ca3c8a
child 181436 289e653a7061a773972b5633e6f53718514012c3
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
reviewerssnorp, smaug
bugs852935
milestone32.0a1
bug 852935 - Add Android gamepad backend. r=snorp, rs=smaug
configure.in
dom/tests/mochitest/general/test_interfaces.html
hal/android/AndroidGamepad.cpp
hal/moz.build
mobile/android/base/AndroidGamepadManager.java
mobile/android/base/BrowserApp.java
mobile/android/base/GeckoAppShell.java
mobile/android/base/GeckoEvent.java
mobile/android/base/gfx/LayerView.java
mobile/android/base/moz.build
mobile/android/installer/package-manifest.in
widget/android/AndroidJavaWrappers.cpp
widget/android/AndroidJavaWrappers.h
widget/android/GeneratedJNIWrappers.cpp
widget/android/GeneratedJNIWrappers.h
widget/android/moz.build
widget/android/nsAppShell.cpp
--- a/configure.in
+++ b/configure.in
@@ -5944,16 +5944,21 @@ dnl ====================================
 MOZ_GAMEPAD=
 MOZ_GAMEPAD_BACKEND=stub
 
 # Gamepad DOM is built on supported platforms by default.
 case "$OS_TARGET" in
      Darwin|WINNT|Linux)
        MOZ_GAMEPAD=1
        ;;
+     Android)
+       if test "$MOZ_WIDGET_TOOLKIT" != "gonk"; then
+         MOZ_GAMEPAD=1
+       fi
+       ;;
      *)
        ;;
 esac
 
 MOZ_ARG_DISABLE_BOOL(gamepad,
 [  --disable-gamepad   Disable gamepad support],
     MOZ_GAMEPAD=,
     MOZ_GAMEPAD=1)
@@ -5968,16 +5973,19 @@ if test "$MOZ_GAMEPAD"; then
         ;;
     Linux)
         MOZ_CHECK_HEADER([linux/joystick.h])
         if test "$ac_cv_header_linux_joystick_h" != "yes"; then
           AC_MSG_ERROR([Can't find header linux/joystick.h, needed for gamepad support. Please install Linux kernel headers or reconfigure with --disable-gamepad to disable gamepad support.])
         fi
         MOZ_GAMEPAD_BACKEND=linux
         ;;
+    Android)
+        MOZ_GAMEPAD_BACKEND=android
+        ;;
     *)
         ;;
    esac
 
   AC_DEFINE(MOZ_GAMEPAD)
 fi
 AC_SUBST(MOZ_GAMEPAD)
 AC_SUBST(MOZ_GAMEPAD_BACKEND)
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -363,25 +363,25 @@ var interfaceNamesInGlobalScope =
     {name: "FMRadio", b2g: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "FocusEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "FormData",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "GainNode",
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    {name: "Gamepad", desktop: true},
+    {name: "Gamepad", b2g: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    {name: "GamepadAxisMoveEvent", desktop: true},
+    {name: "GamepadAxisMoveEvent", b2g: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    {name: "GamepadButtonEvent", desktop: true},
+    {name: "GamepadButtonEvent", b2g: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    {name: "GamepadButton", desktop: true},
+    {name: "GamepadButton", b2g: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    {name: "GamepadEvent", desktop: true},
+    {name: "GamepadEvent", b2g: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "HashChangeEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "History",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "HTMLAnchorElement",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "HTMLAppletElement",
new file mode 100644
--- /dev/null
+++ b/hal/android/AndroidGamepad.cpp
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Hal.h"
+#include "AndroidBridge.h"
+
+using namespace mozilla::hal;
+
+namespace mozilla {
+namespace hal_impl {
+
+void
+StartMonitoringGamepadStatus()
+{
+  mozilla::widget::android::GeckoAppShell::StartMonitoringGamepad();
+}
+
+void
+StopMonitoringGamepadStatus()
+{
+  mozilla::widget::android::GeckoAppShell::StopMonitoringGamepad();
+}
+
+} // hal_impl
+} // mozilla
--- a/hal/moz.build
+++ b/hal/moz.build
@@ -43,16 +43,20 @@ elif CONFIG['MOZ_GAMEPAD_BACKEND'] == 'c
 elif CONFIG['MOZ_GAMEPAD_BACKEND'] == 'windows':
     UNIFIED_SOURCES += [
         'windows/WindowsGamepad.cpp'
     ]
 elif CONFIG['MOZ_GAMEPAD_BACKEND'] == 'linux':
     UNIFIED_SOURCES += [
         'linux/LinuxGamepad.cpp'
     ]
+elif CONFIG['MOZ_GAMEPAD_BACKEND'] == 'android':
+    UNIFIED_SOURCES += [
+        'android/AndroidGamepad.cpp'
+    ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
     UNIFIED_SOURCES += [
         'android/AndroidSensor.cpp',
         'fallback/FallbackAlarm.cpp',
         'fallback/FallbackPower.cpp',
         'linux/LinuxMemory.cpp',
     ]
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/AndroidGamepadManager.java
@@ -0,0 +1,395 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.util.GamepadUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.os.Build;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import java.lang.Math;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+
+public class AndroidGamepadManager {
+    // This is completely arbitrary.
+    private static final float TRIGGER_PRESSED_THRESHOLD = 0.25f;
+    private static final long POLL_TIMER_PERIOD = 1000; // milliseconds
+
+    private static enum Axis {
+        X(MotionEvent.AXIS_X),
+        Y(MotionEvent.AXIS_Y),
+        Z(MotionEvent.AXIS_Z),
+        RZ(MotionEvent.AXIS_RZ);
+
+        public final int axis;
+
+        private Axis(int axis) {
+            this.axis = axis;
+        }
+    };
+
+    // A list of gamepad button mappings. Axes are determined at
+    // runtime, as they vary by Android version.
+    private static enum Trigger {
+        Left(6),
+        Right(7);
+
+        public final int button;
+
+        private Trigger(int button) {
+            this.button = button;
+        }
+    };
+
+    private static final int FIRST_DPAD_BUTTON = 12;
+    // A list of axis number, gamepad button mappings for negative, positive.
+    // Button mappings are added to FIRST_DPAD_BUTTON.
+    private static enum DpadAxis {
+        UpDown(MotionEvent.AXIS_HAT_Y, 0, 1),
+        LeftRight(MotionEvent.AXIS_HAT_X, 2, 3);
+
+        public final int axis;
+        public final int negativeButton;
+        public final int positiveButton;
+
+        private DpadAxis(int axis, int negativeButton, int positiveButton) {
+            this.axis = axis;
+            this.negativeButton = negativeButton;
+            this.positiveButton = positiveButton;
+        }
+    };
+
+    private static enum Button {
+        A(KeyEvent.KEYCODE_BUTTON_A),
+        B(KeyEvent.KEYCODE_BUTTON_B),
+        X(KeyEvent.KEYCODE_BUTTON_X),
+        Y(KeyEvent.KEYCODE_BUTTON_Y),
+        L1(KeyEvent.KEYCODE_BUTTON_L1),
+        R1(KeyEvent.KEYCODE_BUTTON_R1),
+        L2(KeyEvent.KEYCODE_BUTTON_L2),
+        R2(KeyEvent.KEYCODE_BUTTON_R2),
+        SELECT(KeyEvent.KEYCODE_BUTTON_SELECT),
+        START(KeyEvent.KEYCODE_BUTTON_START),
+        THUMBL(KeyEvent.KEYCODE_BUTTON_THUMBL),
+        THUMBR(KeyEvent.KEYCODE_BUTTON_THUMBR),
+        DPAD_UP(KeyEvent.KEYCODE_DPAD_UP),
+        DPAD_DOWN(KeyEvent.KEYCODE_DPAD_DOWN),
+        DPAD_LEFT(KeyEvent.KEYCODE_DPAD_LEFT),
+        DPAD_RIGHT(KeyEvent.KEYCODE_DPAD_RIGHT);
+
+        public final int button;
+
+        private Button(int button) {
+            this.button = button;
+        }
+    };
+
+    private static class Gamepad {
+        // ID from GamepadService
+        public int id;
+        // Retain axis state so we can determine changes.
+        public float axes[];
+        public boolean dpad[];
+        public int triggerAxes[];
+        public float triggers[];
+
+        public Gamepad(int serviceId, int deviceId) {
+            id = serviceId;
+            axes = new float[Axis.values().length];
+            dpad = new boolean[4];
+            triggers = new float[2];
+
+            InputDevice device = InputDevice.getDevice(deviceId);
+            if (device != null) {
+                // LTRIGGER/RTRIGGER don't seem to be exposed on older
+                // versions of Android.
+                if (device.getMotionRange(MotionEvent.AXIS_LTRIGGER) != null && device.getMotionRange(MotionEvent.AXIS_RTRIGGER) != null) {
+                    triggerAxes = new int[]{MotionEvent.AXIS_LTRIGGER,
+                                            MotionEvent.AXIS_RTRIGGER};
+                } else if (device.getMotionRange(MotionEvent.AXIS_BRAKE) != null && device.getMotionRange(MotionEvent.AXIS_GAS) != null) {
+                    triggerAxes = new int[]{MotionEvent.AXIS_BRAKE,
+                                            MotionEvent.AXIS_GAS};
+                } else {
+                    triggerAxes = null;
+                }
+            }
+        }
+    }
+
+    private static boolean sStarted = false;
+    private static HashMap<Integer, Gamepad> sGamepads = null;
+    private static HashMap<Integer, List<KeyEvent>> sPendingGamepads = null;
+    private static InputManager.InputDeviceListener sListener = null;
+    private static Timer sPollTimer = null;
+
+    private AndroidGamepadManager() {
+    }
+
+    public static void startup() {
+        ThreadUtils.assertOnUiThread();
+        if (!sStarted) {
+            sGamepads = new HashMap<Integer, Gamepad>();
+            sPendingGamepads = new HashMap<Integer, List<KeyEvent>>();
+            scanForGamepads();
+            addDeviceListener();
+            sStarted = true;
+        }
+    }
+
+    public static void shutdown() {
+        ThreadUtils.assertOnUiThread();
+        if (sStarted) {
+            removeDeviceListener();
+            sPendingGamepads = null;
+            sGamepads = null;
+            sStarted = false;
+        }
+    }
+
+    public static void gamepadAdded(int deviceId, int serviceId) {
+        ThreadUtils.assertOnUiThread();
+        if (!sStarted) {
+            return;
+        }
+        if (!sPendingGamepads.containsKey(deviceId)) {
+            removeGamepad(deviceId);
+            return;
+        }
+
+        List<KeyEvent> pending = sPendingGamepads.get(deviceId);
+        sPendingGamepads.remove(deviceId);
+        sGamepads.put(deviceId, new Gamepad(serviceId, deviceId));
+        // Handle queued KeyEvents
+        for (KeyEvent ev : pending) {
+            handleKeyEvent(ev);
+        }
+    }
+
+    private static float deadZone(MotionEvent ev, int axis) {
+        if (GamepadUtils.isValueInDeadZone(ev, axis)) {
+            return 0.0f;
+        }
+        return ev.getAxisValue(axis);
+    }
+
+    private static void mapDpadAxis(Gamepad gamepad,
+                                    boolean pressed,
+                                    float value,
+                                    int which) {
+        if (pressed != gamepad.dpad[which]) {
+            gamepad.dpad[which] = pressed;
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(gamepad.id, FIRST_DPAD_BUTTON + which, pressed, Math.abs(value)));
+        }
+    }
+
+    public static boolean handleMotionEvent(MotionEvent ev) {
+        ThreadUtils.assertOnUiThread();
+        if (!sStarted) {
+            return false;
+        }
+
+        if (!sGamepads.containsKey(ev.getDeviceId())) {
+            // Not a device we care about.
+            return false;
+        }
+
+        Gamepad gamepad = sGamepads.get(ev.getDeviceId());
+        // First check the analog stick axes
+        boolean[] valid = new boolean[Axis.values().length];
+        float[] axes = new float[Axis.values().length];
+        boolean anyValidAxes = false;
+        for (Axis axis : Axis.values()) {
+            float value = deadZone(ev, axis.axis);
+            int i = axis.ordinal();
+            if (value != gamepad.axes[i]) {
+                axes[i] = value;
+                gamepad.axes[i] = value;
+                valid[i] = true;
+                anyValidAxes = true;
+            }
+        }
+        if (anyValidAxes) {
+            // Send an axismove event.
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAxisEvent(gamepad.id, valid, axes));
+        }
+
+        // Map triggers to buttons.
+        if (gamepad.triggerAxes != null) {
+            for (Trigger trigger : Trigger.values()) {
+                int i = trigger.ordinal();
+                int axis = gamepad.triggerAxes[i];
+                float value = deadZone(ev, axis);
+                if (value != gamepad.triggers[i]) {
+                    gamepad.triggers[i] = value;
+                    boolean pressed = value > TRIGGER_PRESSED_THRESHOLD;
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(gamepad.id, trigger.button, pressed, value));
+                }
+            }
+        }
+        // Map d-pad to buttons.
+        for (DpadAxis dpadaxis : DpadAxis.values()) {
+            float value = deadZone(ev, dpadaxis.axis);
+            mapDpadAxis(gamepad, value < 0.0f, value, dpadaxis.negativeButton);
+            mapDpadAxis(gamepad, value > 0.0f, value, dpadaxis.positiveButton);
+        }
+        return true;
+    }
+
+    public static boolean handleKeyEvent(KeyEvent ev) {
+        ThreadUtils.assertOnUiThread();
+        if (!sStarted) {
+            return false;
+        }
+
+        int deviceId = ev.getDeviceId();
+        if (sPendingGamepads.containsKey(deviceId)) {
+            // Queue up key events for pending devices.
+            sPendingGamepads.get(deviceId).add(ev);
+            return true;
+        } else if (!sGamepads.containsKey(deviceId)) {
+            InputDevice device = ev.getDevice();
+            if (device != null &&
+                (device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
+                // This is a gamepad we haven't seen yet.
+                addGamepad(device);
+                sPendingGamepads.get(deviceId).add(ev);
+                return true;
+            }
+            // Not a device we care about.
+            return false;
+        }
+
+        int key = -1;
+        for (Button button : Button.values()) {
+            if (button.button == ev.getKeyCode()) {
+                key = button.ordinal();
+                break;
+            }
+        }
+        if (key == -1) {
+            // Not a key we know how to handle.
+            return false;
+        }
+        if (ev.getRepeatCount() > 0) {
+            // We would handle this key, but we're not interested in
+            // repeats. Eat it.
+            return true;
+        }
+
+        Gamepad gamepad = sGamepads.get(deviceId);
+        boolean pressed = ev.getAction() == KeyEvent.ACTION_DOWN;
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadButtonEvent(gamepad.id, key, pressed, pressed ? 1.0f : 0.0f));
+        return true;
+    }
+
+    private static void scanForGamepads() {
+        if (Build.VERSION.SDK_INT < 9) {
+            return;
+        }
+
+        int[] deviceIds = InputDevice.getDeviceIds();
+        if (deviceIds == null) {
+            return;
+        }
+        for (int i=0; i < deviceIds.length; i++) {
+            InputDevice device = InputDevice.getDevice(deviceIds[i]);
+            if (device == null) {
+                continue;
+            }
+            if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD) {
+                continue;
+            }
+            addGamepad(device);
+        }
+    }
+
+    private static void addGamepad(InputDevice device) {
+        //TODO: when we're using a newer SDK version, use these.
+        //if (Build.VERSION.SDK_INT >= 12) {
+        //int vid = device.getVendorId();
+        //int pid = device.getProductId();
+        //}
+        sPendingGamepads.put(device.getId(), new ArrayList<KeyEvent>());
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAddRemoveEvent(device.getId(), true));
+    }
+
+    private static void removeGamepad(int deviceId) {
+        Gamepad gamepad = sGamepads.get(deviceId);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createGamepadAddRemoveEvent(gamepad.id, false));
+        sGamepads.remove(deviceId);
+    }
+
+    private static void addDeviceListener() {
+        if (Build.VERSION.SDK_INT < 16) {
+            // Poll known gamepads to see if they've disappeared.
+            sPollTimer = new Timer();
+            sPollTimer.scheduleAtFixedRate(new TimerTask() {
+                    public void run() {
+                        for (Integer deviceId : sGamepads.keySet()) {
+                            if (InputDevice.getDevice(deviceId) == null) {
+                                removeGamepad(deviceId);
+                            }
+                        }
+                    }
+                }, POLL_TIMER_PERIOD, POLL_TIMER_PERIOD);
+            return;
+        }
+        sListener = new InputManager.InputDeviceListener() {
+                public void onInputDeviceAdded(int deviceId) {
+                    InputDevice device = InputDevice.getDevice(deviceId);
+                    if (device == null) {
+                        return;
+                    }
+                    if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
+                        addGamepad(device);
+                    }
+                }
+
+                public void onInputDeviceRemoved(int deviceId) {
+                    if (sPendingGamepads.containsKey(deviceId)) {
+                        // Got removed before Gecko's ack reached us.
+                        // gamepadAdded will deal with it.
+                        sPendingGamepads.remove(deviceId);
+                        return;
+                    }
+                    if (sGamepads.containsKey(deviceId)) {
+                        removeGamepad(deviceId);
+                    }
+                }
+
+                public void onInputDeviceChanged(int deviceId) {
+                }
+            };
+        ((InputManager)GeckoAppShell.getContext().getSystemService(Context.INPUT_SERVICE)).registerInputDeviceListener(sListener, ThreadUtils.getUiHandler());
+    }
+
+    private static void removeDeviceListener() {
+        if (Build.VERSION.SDK_INT < 16) {
+            if (sPollTimer != null) {
+                sPollTimer.cancel();
+                sPollTimer = null;
+            }
+            return;
+        }
+        ((InputManager)GeckoAppShell.getContext().getSystemService(Context.INPUT_SERVICE)).unregisterInputDeviceListener(sListener);
+        sListener = null;
+    }
+}
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -9,16 +9,17 @@ import java.io.File;
 import java.io.FileNotFoundException;
 import java.net.URLEncoder;
 import java.util.EnumSet;
 import java.util.Vector;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.mozilla.gecko.AndroidGamepadManager;
 import org.mozilla.gecko.DynamicToolbar.PinReason;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
 import org.mozilla.gecko.db.BrowserDB;
@@ -263,16 +264,20 @@ abstract public class BrowserApp extends
                 }
                 break;
         }
         super.onTabChanged(tab, msg, data);
     }
 
     @Override
     public boolean onKey(View v, int keyCode, KeyEvent event) {
+        if (AndroidGamepadManager.handleKeyEvent(event)) {
+            return true;
+        }
+
         // Global onKey handler. This is called if the focused UI doesn't
         // handle the key event, and before Gecko swallows the events.
         if (event.getAction() != KeyEvent.ACTION_DOWN) {
             return false;
         }
 
         // Gamepad support only exists in API-level >= 9
         if (Build.VERSION.SDK_INT >= 9 &&
@@ -352,16 +357,24 @@ abstract public class BrowserApp extends
 
         if (mBrowserToolbar.onKey(keyCode, event)) {
             return true;
         }
 
         return super.onKeyDown(keyCode, event);
     }
 
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (AndroidGamepadManager.handleKeyEvent(event)) {
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
     void handleReaderListStatusRequest(final String url) {
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
                 final int inReadingList = BrowserDB.isReadingListItem(getContentResolver(), url) ? 1 : 0;
 
                 final JSONObject json = new JSONObject();
                 try {
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -694,16 +694,53 @@ public class GeckoAppShell
                 sm.unregisterListener(gi.getSensorEventListener(), gGyroscopeSensor);
             break;
         default:
             Log.w(LOGTAG, "Error! Can't disable unknown SENSOR type " + aSensortype);
         }
     }
 
     @WrapElementForJNI
+    public static void startMonitoringGamepad() {
+        if (Build.VERSION.SDK_INT < 9) {
+            return;
+        }
+        ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    AndroidGamepadManager.startup();
+                }
+            });
+    }
+
+    @WrapElementForJNI
+    public static void stopMonitoringGamepad() {
+        if (Build.VERSION.SDK_INT < 9) {
+            return;
+        }
+
+        ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    AndroidGamepadManager.shutdown();
+                }
+            });
+    }
+
+    @WrapElementForJNI
+    public static void gamepadAdded(final int device_id, final int service_id) {
+        ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    AndroidGamepadManager.gamepadAdded(device_id, service_id);
+                }
+            });
+    }
+
+    @WrapElementForJNI
     public static void moveTaskToBack() {
         if (getGeckoInterface() != null)
             getGeckoInterface().getActivity().moveTaskToBack(true);
     }
 
     public static void returnIMEQueryResult(String result, int selectionStart, int selectionLength) {
         // This method may be called from JNI to report Gecko's current selection indexes, but
         // Native Fennec doesn't care because the Java code already knows the selection indexes.
--- a/mobile/android/base/GeckoEvent.java
+++ b/mobile/android/base/GeckoEvent.java
@@ -104,17 +104,19 @@ public class GeckoEvent {
         LOW_MEMORY(35),
         NETWORK_LINK_CHANGE(36),
         TELEMETRY_HISTOGRAM_ADD(37),
         PREFERENCES_OBSERVE(39),
         PREFERENCES_GET(40),
         PREFERENCES_REMOVE_OBSERVERS(41),
         TELEMETRY_UI_SESSION_START(42),
         TELEMETRY_UI_SESSION_STOP(43),
-        TELEMETRY_UI_EVENT(44);
+        TELEMETRY_UI_EVENT(44),
+        GAMEPAD_ADDREMOVE(45),
+        GAMEPAD_DATA(46);
 
         public final int value;
 
         private NativeGeckoEvent(int value) {
             this.value = value;
         }
     }
 
@@ -174,16 +176,22 @@ public class GeckoEvent {
     public static final int IME_RANGE_FORECOLOR = 2;
     public static final int IME_RANGE_BACKCOLOR = 4;
     public static final int IME_RANGE_LINECOLOR = 8;
 
     public static final int ACTION_MAGNIFY_START = 11;
     public static final int ACTION_MAGNIFY = 12;
     public static final int ACTION_MAGNIFY_END = 13;
 
+    public static final int ACTION_GAMEPAD_ADDED = 1;
+    public static final int ACTION_GAMEPAD_REMOVED = 2;
+
+    public static final int ACTION_GAMEPAD_BUTTON = 1;
+    public static final int ACTION_GAMEPAD_AXES = 2;
+
     private final int mType;
     private int mAction;
     private boolean mAckNeeded;
     private long mTime;
     private Point[] mPoints;
     private int[] mPointIndicies;
     private int mPointerIndex; // index of the point that has changed
     private float[] mOrientations;
@@ -226,16 +234,22 @@ public class GeckoEvent {
 
     private short mScreenOrientation;
 
     private ByteBuffer mBuffer;
 
     private int mWidth;
     private int mHeight;
 
+    private int mID;
+    private int mGamepadButton;
+    private boolean mGamepadButtonPressed;
+    private float mGamepadButtonValue;
+    private float[] mGamepadValues;
+
     private String[] mPrefNames;
 
     private GeckoEvent(NativeGeckoEvent event) {
         mType = event.value;
     }
 
     public static GeckoEvent createAppBackgroundingEvent() {
         return GeckoEvent.get(NativeGeckoEvent.APP_BACKGROUNDING);
@@ -808,12 +822,53 @@ public class GeckoEvent {
         GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.TELEMETRY_UI_EVENT);
         event.mData = action;
         event.mCharacters = method;
         event.mCharactersExtra = extras;
         event.mTime = timestamp;
         return event;
     }
 
+    public static GeckoEvent createGamepadAddRemoveEvent(int id, boolean added) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_ADDREMOVE);
+        event.mID = id;
+        event.mAction = added ? ACTION_GAMEPAD_ADDED : ACTION_GAMEPAD_REMOVED;
+        return event;
+    }
+
+    private static int boolArrayToBitfield(boolean[] array) {
+        int bits = 0;
+        for (int i = 0; i < array.length; i++) {
+            if (array[i]) {
+                bits |= 1<<i;
+            }
+        }
+        return bits;
+    }
+
+    public static GeckoEvent createGamepadButtonEvent(int id,
+                                                      int which,
+                                                      boolean pressed,
+                                                      float value) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_DATA);
+        event.mID = id;
+        event.mAction = ACTION_GAMEPAD_BUTTON;
+        event.mGamepadButton = which;
+        event.mGamepadButtonPressed = pressed;
+        event.mGamepadButtonValue = value;
+        return event;
+    }
+
+    public static GeckoEvent createGamepadAxisEvent(int id, boolean[] valid,
+                                                    float[] values) {
+        GeckoEvent event = GeckoEvent.get(NativeGeckoEvent.GAMEPAD_DATA);
+        event.mID = id;
+        event.mAction = ACTION_GAMEPAD_AXES;
+        event.mFlags = boolArrayToBitfield(valid);
+        event.mCount = values.length;
+        event.mGamepadValues = values;
+        return event;
+    }
+
     public void setAckNeeded(boolean ackNeeded) {
         mAckNeeded = ackNeeded;
     }
 }
--- a/mobile/android/base/gfx/LayerView.java
+++ b/mobile/android/base/gfx/LayerView.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.gfx;
 
+import org.mozilla.gecko.AndroidGamepadManager;
 import org.mozilla.gecko.GeckoAccessibility;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.PrefsHelper;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.TouchEventInterceptor;
@@ -286,16 +287,19 @@ public class LayerView extends FrameLayo
         if (runTouchInterceptors(event, true)) {
             return true;
         }
         return false;
     }
 
     @Override
     public boolean onGenericMotionEvent(MotionEvent event) {
+        if (AndroidGamepadManager.handleMotionEvent(event)) {
+            return true;
+        }
         if (mPanZoomController != null && mPanZoomController.onMotionEvent(event)) {
             return true;
         }
         return false;
     }
 
     @Override
     protected void onAttachedToWindow() {
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -104,16 +104,17 @@ if CONFIG['MOZ_WEBRTC']:
 
 gbjar = add_java_jar('gecko-browser')
 gbjar.sources += [
     'AboutPages.java',
     'ActionModeCompat.java',
     'ActionModeCompatView.java',
     'ActivityHandlerHelper.java',
     'AlertNotification.java',
+    'AndroidGamepadManager.java',
     'animation/AnimationUtils.java',
     'animation/AnimatorProxy.java',
     'animation/HeightChangeAnimation.java',
     'animation/PropertyAnimator.java',
     'animation/Rotate3DAnimation.java',
     'animation/ViewHelper.java',
     'ANRReporter.java',
     'AppNotificationClient.java',
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -164,16 +164,19 @@
 @BINPATH@/components/dom_traversal.xpt
 @BINPATH@/components/dom_views.xpt
 #ifdef MOZ_WEBSPEECH
 @BINPATH@/components/dom_webspeechrecognition.xpt
 #endif
 @BINPATH@/components/dom_xbl.xpt
 @BINPATH@/components/dom_xpath.xpt
 @BINPATH@/components/dom_xul.xpt
+#ifdef MOZ_GAMEPAD
+@BINPATH@/components/dom_gamepad.xpt
+#endif
 @BINPATH@/components/downloads.xpt
 @BINPATH@/components/editor.xpt
 @BINPATH@/components/embed_base.xpt
 @BINPATH@/components/extensions.xpt
 @BINPATH@/components/exthandler.xpt
 @BINPATH@/components/exthelper.xpt
 @BINPATH@/components/fastfind.xpt
 @BINPATH@/components/feeds.xpt
--- a/widget/android/AndroidJavaWrappers.cpp
+++ b/widget/android/AndroidJavaWrappers.cpp
@@ -57,16 +57,21 @@ jfieldID AndroidGeckoEvent::jRangeLineCo
 jfieldID AndroidGeckoEvent::jLocationField = 0;
 jfieldID AndroidGeckoEvent::jConnectionTypeField = 0;
 jfieldID AndroidGeckoEvent::jIsWifiField = 0;
 jfieldID AndroidGeckoEvent::jDHCPGatewayField = 0;
 jfieldID AndroidGeckoEvent::jScreenOrientationField = 0;
 jfieldID AndroidGeckoEvent::jByteBufferField = 0;
 jfieldID AndroidGeckoEvent::jWidthField = 0;
 jfieldID AndroidGeckoEvent::jHeightField = 0;
+jfieldID AndroidGeckoEvent::jIDField = 0;
+jfieldID AndroidGeckoEvent::jGamepadButtonField = 0;
+jfieldID AndroidGeckoEvent::jGamepadButtonPressedField = 0;
+jfieldID AndroidGeckoEvent::jGamepadButtonValueField = 0;
+jfieldID AndroidGeckoEvent::jGamepadValuesField = 0;
 jfieldID AndroidGeckoEvent::jPrefNamesField = 0;
 
 jclass AndroidGeckoEvent::jDomKeyLocationClass = 0;
 jfieldID AndroidGeckoEvent::jDomKeyLocationValueField = 0;
 
 jclass AndroidPoint::jPointClass = 0;
 jfieldID AndroidPoint::jXField = 0;
 jfieldID AndroidPoint::jYField = 0;
@@ -162,16 +167,21 @@ AndroidGeckoEvent::InitGeckoEventClass(J
     jLocationField = getField("mLocation", "Landroid/location/Location;");
     jConnectionTypeField = getField("mConnectionType", "I");
     jIsWifiField = getField("mIsWifi", "Z");
     jDHCPGatewayField = getField("mDHCPGateway", "I");
     jScreenOrientationField = getField("mScreenOrientation", "S");
     jByteBufferField = getField("mBuffer", "Ljava/nio/ByteBuffer;");
     jWidthField = getField("mWidth", "I");
     jHeightField = getField("mHeight", "I");
+    jIDField = getField("mID", "I");
+    jGamepadButtonField = getField("mGamepadButton", "I");
+    jGamepadButtonPressedField = getField("mGamepadButtonPressed", "Z");
+    jGamepadButtonValueField = getField("mGamepadButtonValue", "F");
+    jGamepadValuesField = getField("mGamepadValues", "[F");
     jPrefNamesField = getField("mPrefNames", "[Ljava/lang/String;");
 
     // Init GeckoEvent.DomKeyLocation enum
     jDomKeyLocationClass = getClassGlobalRef("org/mozilla/gecko/GeckoEvent$DomKeyLocation");
     jDomKeyLocationValueField = getField("value", "I");
 }
 
 void
@@ -581,16 +591,36 @@ AndroidGeckoEvent::Init(JNIEnv *jenv, jo
         case TELEMETRY_UI_EVENT: {
             ReadCharactersField(jenv);
             ReadCharactersExtraField(jenv);
             ReadDataField(jenv);
             mTime = jenv->GetLongField(jobj, jTimeField);
             break;
         }
 
+        case GAMEPAD_ADDREMOVE: {
+            mID = jenv->GetIntField(jobj, jIDField);
+            break;
+        }
+
+        case GAMEPAD_DATA: {
+            mID = jenv->GetIntField(jobj, jIDField);
+            if (mAction == ACTION_GAMEPAD_BUTTON) {
+                mGamepadButton = jenv->GetIntField(jobj, jGamepadButtonField);
+                mGamepadButtonPressed = jenv->GetBooleanField(jobj, jGamepadButtonPressedField);
+                mGamepadButtonValue = jenv->GetFloatField(jobj, jGamepadButtonValueField);
+            } else if (mAction == ACTION_GAMEPAD_AXES) {
+                // Flags is a bitfield of valid entries in gamepadvalues
+                mFlags = jenv->GetIntField(jobj, jFlagsField);
+                mCount = jenv->GetIntField(jobj, jCountField);
+                ReadFloatArray(mGamepadValues, jenv, jGamepadValuesField, mCount);
+            }
+            break;
+        }
+
         case PREFERENCES_OBSERVE:
         case PREFERENCES_GET: {
             ReadStringArray(mPrefNames, jenv, jPrefNamesField);
             mCount = jenv->GetIntField(jobj, jCountField);
             break;
         }
 
         case PREFERENCES_REMOVE_OBSERVERS: {
--- a/widget/android/AndroidJavaWrappers.h
+++ b/widget/android/AndroidJavaWrappers.h
@@ -532,16 +532,21 @@ public:
     nsGeoPosition* GeoPosition() { return mGeoPosition; }
     int ConnectionType() { return mConnectionType; }
     bool IsWifi() { return mIsWifi; }
     int DHCPGateway() { return mDHCPGateway; }
     short ScreenOrientation() { return mScreenOrientation; }
     RefCountedJavaObject* ByteBuffer() { return mByteBuffer; }
     int Width() { return mWidth; }
     int Height() { return mHeight; }
+    int ID() { return mID; }
+    int GamepadButton() { return mGamepadButton; }
+    bool GamepadButtonPressed() { return mGamepadButtonPressed; }
+    float GamepadButtonValue() { return mGamepadButtonValue; }
+    const nsTArray<float>& GamepadValues() { return mGamepadValues; }
     int RequestId() { return mCount; } // for convenience
     WidgetTouchEvent MakeTouchEvent(nsIWidget* widget);
     MultiTouchInput MakeMultiTouchInput(nsIWidget* widget);
     WidgetMouseEvent MakeMouseEvent(nsIWidget* widget);
     void UnionRect(nsIntRect const& aRect);
     nsIObserver *Observer() { return mObserver; }
 
 protected:
@@ -569,16 +574,21 @@ protected:
     nsString mCharacters, mCharactersExtra, mData;
     nsRefPtr<nsGeoPosition> mGeoPosition;
     int mConnectionType;
     bool mIsWifi;
     int mDHCPGateway;
     short mScreenOrientation;
     nsRefPtr<RefCountedJavaObject> mByteBuffer;
     int mWidth, mHeight;
+    int mID;
+    int mGamepadButton;
+    bool mGamepadButtonPressed;
+    float mGamepadButtonValue;
+    nsTArray<float> mGamepadValues;
     nsCOMPtr<nsIObserver> mObserver;
     nsTArray<nsString> mPrefNames;
 
     void ReadIntArray(nsTArray<int> &aVals,
                       JNIEnv *jenv,
                       jfieldID field,
                       int32_t count);
     void ReadFloatArray(nsTArray<float> &aVals,
@@ -647,16 +657,22 @@ protected:
     static jfieldID jDHCPGatewayField;
 
     static jfieldID jScreenOrientationField;
     static jfieldID jByteBufferField;
 
     static jfieldID jWidthField;
     static jfieldID jHeightField;
 
+    static jfieldID jIDField;
+    static jfieldID jGamepadButtonField;
+    static jfieldID jGamepadButtonPressedField;
+    static jfieldID jGamepadButtonValueField;
+    static jfieldID jGamepadValuesField;
+
     static jclass jDomKeyLocationClass;
     static jfieldID jDomKeyLocationValueField;
 
 public:
     enum {
         NATIVE_POKE = 0,
         KEY_EVENT = 1,
         MOTION_EVENT = 2,
@@ -688,16 +704,18 @@ public:
         TELEMETRY_HISTOGRAM_ADD = 37,
         ADD_OBSERVER = 38,
         PREFERENCES_OBSERVE = 39,
         PREFERENCES_GET = 40,
         PREFERENCES_REMOVE_OBSERVERS = 41,
         TELEMETRY_UI_SESSION_START = 42,
         TELEMETRY_UI_SESSION_STOP = 43,
         TELEMETRY_UI_EVENT = 44,
+        GAMEPAD_ADDREMOVE = 45,
+        GAMEPAD_DATA = 46,
         dummy_java_enum_list_end
     };
 
     enum {
         // Memory pressure levels. Keep these in sync with those in MemoryMonitor.java.
         MEMORY_PRESSURE_NONE = 0,
         MEMORY_PRESSURE_CLEANUP = 1,
         MEMORY_PRESSURE_LOW = 2,
@@ -714,16 +732,26 @@ public:
         IME_REPLACE_TEXT = 1,
         IME_SET_SELECTION = 2,
         IME_ADD_COMPOSITION_RANGE = 3,
         IME_UPDATE_COMPOSITION = 4,
         IME_REMOVE_COMPOSITION = 5,
         IME_ACKNOWLEDGE_FOCUS = 6,
         dummy_ime_enum_list_end
     };
+
+    enum {
+        ACTION_GAMEPAD_ADDED = 1,
+        ACTION_GAMEPAD_REMOVED = 2
+    };
+
+    enum {
+        ACTION_GAMEPAD_BUTTON = 1,
+        ACTION_GAMEPAD_AXES = 2
+    };
 };
 
 class nsJNIString : public nsString
 {
 public:
     nsJNIString(jstring jstr, JNIEnv *jenv);
 };
 
--- a/widget/android/GeneratedJNIWrappers.cpp
+++ b/widget/android/GeneratedJNIWrappers.cpp
@@ -28,16 +28,17 @@ jmethodID GeckoAppShell::jDisableNetwork
 jmethodID GeckoAppShell::jDisableScreenOrientationNotifications = 0;
 jmethodID GeckoAppShell::jDisableSensor = 0;
 jmethodID GeckoAppShell::jEnableBatteryNotifications = 0;
 jmethodID GeckoAppShell::jEnableLocation = 0;
 jmethodID GeckoAppShell::jEnableLocationHighAccuracy = 0;
 jmethodID GeckoAppShell::jEnableNetworkNotifications = 0;
 jmethodID GeckoAppShell::jEnableScreenOrientationNotifications = 0;
 jmethodID GeckoAppShell::jEnableSensor = 0;
+jmethodID GeckoAppShell::jGamepadAdded = 0;
 jmethodID GeckoAppShell::jGetContext = 0;
 jmethodID GeckoAppShell::jGetCurrentBatteryInformationWrapper = 0;
 jmethodID GeckoAppShell::jGetCurrentNetworkInformationWrapper = 0;
 jmethodID GeckoAppShell::jGetDensity = 0;
 jmethodID GeckoAppShell::jGetDpiWrapper = 0;
 jmethodID GeckoAppShell::jGetExtensionFromMimeTypeWrapper = 0;
 jmethodID GeckoAppShell::jGetHandlersForMimeTypeWrapper = 0;
 jmethodID GeckoAppShell::jGetHandlersForURLWrapper = 0;
@@ -77,16 +78,18 @@ jmethodID GeckoAppShell::jRemovePluginVi
 jmethodID GeckoAppShell::jScanMedia = 0;
 jmethodID GeckoAppShell::jScheduleRestart = 0;
 jmethodID GeckoAppShell::jSendMessageWrapper = 0;
 jmethodID GeckoAppShell::jSetFullScreen = 0;
 jmethodID GeckoAppShell::jSetKeepScreenOn = 0;
 jmethodID GeckoAppShell::jSetURITitle = 0;
 jmethodID GeckoAppShell::jShowAlertNotificationWrapper = 0;
 jmethodID GeckoAppShell::jShowInputMethodPicker = 0;
+jmethodID GeckoAppShell::jStartMonitoringGamepad = 0;
+jmethodID GeckoAppShell::jStopMonitoringGamepad = 0;
 jmethodID GeckoAppShell::jUnlockProfile = 0;
 jmethodID GeckoAppShell::jUnlockScreenOrientation = 0;
 jmethodID GeckoAppShell::jUnregisterSurfaceTextureFrameListener = 0;
 jmethodID GeckoAppShell::jVibrate1 = 0;
 jmethodID GeckoAppShell::jVibrateA = 0;
 void GeckoAppShell::InitStubs(JNIEnv *jEnv) {
     initInit();
 
@@ -107,16 +110,17 @@ void GeckoAppShell::InitStubs(JNIEnv *jE
     jDisableScreenOrientationNotifications = getStaticMethod("disableScreenOrientationNotifications", "()V");
     jDisableSensor = getStaticMethod("disableSensor", "(I)V");
     jEnableBatteryNotifications = getStaticMethod("enableBatteryNotifications", "()V");
     jEnableLocation = getStaticMethod("enableLocation", "(Z)V");
     jEnableLocationHighAccuracy = getStaticMethod("enableLocationHighAccuracy", "(Z)V");
     jEnableNetworkNotifications = getStaticMethod("enableNetworkNotifications", "()V");
     jEnableScreenOrientationNotifications = getStaticMethod("enableScreenOrientationNotifications", "()V");
     jEnableSensor = getStaticMethod("enableSensor", "(I)V");
+    jGamepadAdded = getStaticMethod("gamepadAdded", "(II)V");
     jGetContext = getStaticMethod("getContext", "()Landroid/content/Context;");
     jGetCurrentBatteryInformationWrapper = getStaticMethod("getCurrentBatteryInformation", "()[D");
     jGetCurrentNetworkInformationWrapper = getStaticMethod("getCurrentNetworkInformation", "()[D");
     jGetDensity = getStaticMethod("getDensity", "()F");
     jGetDpiWrapper = getStaticMethod("getDpi", "()I");
     jGetExtensionFromMimeTypeWrapper = getStaticMethod("getExtensionFromMimeType", "(Ljava/lang/String;)Ljava/lang/String;");
     jGetHandlersForMimeTypeWrapper = getStaticMethod("getHandlersForMimeType", "(Ljava/lang/String;Ljava/lang/String;)[Ljava/lang/String;");
     jGetHandlersForURLWrapper = getStaticMethod("getHandlersForURL", "(Ljava/lang/String;Ljava/lang/String;)[Ljava/lang/String;");
@@ -156,16 +160,18 @@ void GeckoAppShell::InitStubs(JNIEnv *jE
     jScanMedia = getStaticMethod("scanMedia", "(Ljava/lang/String;Ljava/lang/String;)V");
     jScheduleRestart = getStaticMethod("scheduleRestart", "()V");
     jSendMessageWrapper = getStaticMethod("sendMessage", "(Ljava/lang/String;Ljava/lang/String;I)V");
     jSetFullScreen = getStaticMethod("setFullScreen", "(Z)V");
     jSetKeepScreenOn = getStaticMethod("setKeepScreenOn", "(Z)V");
     jSetURITitle = getStaticMethod("setUriTitle", "(Ljava/lang/String;Ljava/lang/String;)V");
     jShowAlertNotificationWrapper = getStaticMethod("showAlertNotification", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
     jShowInputMethodPicker = getStaticMethod("showInputMethodPicker", "()V");
+    jStartMonitoringGamepad = getStaticMethod("startMonitoringGamepad", "()V");
+    jStopMonitoringGamepad = getStaticMethod("stopMonitoringGamepad", "()V");
     jUnlockProfile = getStaticMethod("unlockProfile", "()Z");
     jUnlockScreenOrientation = getStaticMethod("unlockScreenOrientation", "()V");
     jUnregisterSurfaceTextureFrameListener = getStaticMethod("unregisterSurfaceTextureFrameListener", "(Ljava/lang/Object;)V");
     jVibrate1 = getStaticMethod("vibrate", "(J)V");
     jVibrateA = getStaticMethod("vibrate", "([JI)V");
 }
 
 GeckoAppShell* GeckoAppShell::Wrap(jobject obj) {
@@ -455,16 +461,28 @@ void GeckoAppShell::EnableSensor(int32_t
         MOZ_CRASH("Exception should have caused crash.");
     }
 
     env->CallStaticVoidMethod(mGeckoAppShellClass, jEnableSensor, a0);
     AndroidBridge::HandleUncaughtException(env);
     env->PopLocalFrame(nullptr);
 }
 
+void GeckoAppShell::GamepadAdded(int32_t a0, int32_t a1) {
+    JNIEnv *env = AndroidBridge::GetJNIEnv();
+    if (env->PushLocalFrame(0) != 0) {
+        AndroidBridge::HandleUncaughtException(env);
+        MOZ_CRASH("Exception should have caused crash.");
+    }
+
+    env->CallStaticVoidMethod(mGeckoAppShellClass, jGamepadAdded, a0, a1);
+    AndroidBridge::HandleUncaughtException(env);
+    env->PopLocalFrame(nullptr);
+}
+
 jobject GeckoAppShell::GetContext() {
     JNIEnv *env = GetJNIForThread();
     if (env->PushLocalFrame(1) != 0) {
         AndroidBridge::HandleUncaughtException(env);
         MOZ_CRASH("Exception should have caused crash.");
     }
 
     jobject temp = env->CallStaticObjectMethod(mGeckoAppShellClass, jGetContext);
@@ -1134,16 +1152,40 @@ void GeckoAppShell::ShowInputMethodPicke
         MOZ_CRASH("Exception should have caused crash.");
     }
 
     env->CallStaticVoidMethod(mGeckoAppShellClass, jShowInputMethodPicker);
     AndroidBridge::HandleUncaughtException(env);
     env->PopLocalFrame(nullptr);
 }
 
+void GeckoAppShell::StartMonitoringGamepad() {
+    JNIEnv *env = AndroidBridge::GetJNIEnv();
+    if (env->PushLocalFrame(0) != 0) {
+        AndroidBridge::HandleUncaughtException(env);
+        MOZ_CRASH("Exception should have caused crash.");
+    }
+
+    env->CallStaticVoidMethod(mGeckoAppShellClass, jStartMonitoringGamepad);
+    AndroidBridge::HandleUncaughtException(env);
+    env->PopLocalFrame(nullptr);
+}
+
+void GeckoAppShell::StopMonitoringGamepad() {
+    JNIEnv *env = AndroidBridge::GetJNIEnv();
+    if (env->PushLocalFrame(0) != 0) {
+        AndroidBridge::HandleUncaughtException(env);
+        MOZ_CRASH("Exception should have caused crash.");
+    }
+
+    env->CallStaticVoidMethod(mGeckoAppShellClass, jStopMonitoringGamepad);
+    AndroidBridge::HandleUncaughtException(env);
+    env->PopLocalFrame(nullptr);
+}
+
 bool GeckoAppShell::UnlockProfile() {
     JNIEnv *env = AndroidBridge::GetJNIEnv();
     if (env->PushLocalFrame(0) != 0) {
         AndroidBridge::HandleUncaughtException(env);
         MOZ_CRASH("Exception should have caused crash.");
     }
 
     bool temp = env->CallStaticBooleanMethod(mGeckoAppShellClass, jUnlockProfile);
--- a/widget/android/GeneratedJNIWrappers.h
+++ b/widget/android/GeneratedJNIWrappers.h
@@ -35,16 +35,17 @@ public:
     static void DisableScreenOrientationNotifications();
     static void DisableSensor(int32_t a0);
     static void EnableBatteryNotifications();
     static void EnableLocation(bool a0);
     static void EnableLocationHighAccuracy(bool a0);
     static void EnableNetworkNotifications();
     static void EnableScreenOrientationNotifications();
     static void EnableSensor(int32_t a0);
+    static void GamepadAdded(int32_t a0, int32_t a1);
     static jobject GetContext();
     static jdoubleArray GetCurrentBatteryInformationWrapper();
     static jdoubleArray GetCurrentNetworkInformationWrapper();
     static jfloat GetDensity();
     static int32_t GetDpiWrapper();
     static jstring GetExtensionFromMimeTypeWrapper(const nsAString& a0);
     static jobjectArray GetHandlersForMimeTypeWrapper(const nsAString& a0, const nsAString& a1);
     static jobjectArray GetHandlersForURLWrapper(const nsAString& a0, const nsAString& a1);
@@ -84,16 +85,18 @@ public:
     static void ScanMedia(const nsAString& a0, const nsAString& a1);
     static void ScheduleRestart();
     static void SendMessageWrapper(const nsAString& a0, const nsAString& a1, int32_t a2);
     static void SetFullScreen(bool a0);
     static void SetKeepScreenOn(bool a0);
     static void SetURITitle(const nsAString& a0, const nsAString& a1);
     static void ShowAlertNotificationWrapper(const nsAString& a0, const nsAString& a1, const nsAString& a2, const nsAString& a3, const nsAString& a4);
     static void ShowInputMethodPicker();
+    static void StartMonitoringGamepad();
+    static void StopMonitoringGamepad();
     static bool UnlockProfile();
     static void UnlockScreenOrientation();
     static void UnregisterSurfaceTextureFrameListener(jobject a0);
     static void Vibrate1(int64_t a0);
     static void VibrateA(jlongArray a0, int32_t a1);
     GeckoAppShell() : AutoGlobalWrappedJavaObject() {};
 protected:
     static jclass mGeckoAppShellClass;
@@ -113,16 +116,17 @@ protected:
     static jmethodID jDisableScreenOrientationNotifications;
     static jmethodID jDisableSensor;
     static jmethodID jEnableBatteryNotifications;
     static jmethodID jEnableLocation;
     static jmethodID jEnableLocationHighAccuracy;
     static jmethodID jEnableNetworkNotifications;
     static jmethodID jEnableScreenOrientationNotifications;
     static jmethodID jEnableSensor;
+    static jmethodID jGamepadAdded;
     static jmethodID jGetContext;
     static jmethodID jGetCurrentBatteryInformationWrapper;
     static jmethodID jGetCurrentNetworkInformationWrapper;
     static jmethodID jGetDensity;
     static jmethodID jGetDpiWrapper;
     static jmethodID jGetExtensionFromMimeTypeWrapper;
     static jmethodID jGetHandlersForMimeTypeWrapper;
     static jmethodID jGetHandlersForURLWrapper;
@@ -162,16 +166,18 @@ protected:
     static jmethodID jScanMedia;
     static jmethodID jScheduleRestart;
     static jmethodID jSendMessageWrapper;
     static jmethodID jSetFullScreen;
     static jmethodID jSetKeepScreenOn;
     static jmethodID jSetURITitle;
     static jmethodID jShowAlertNotificationWrapper;
     static jmethodID jShowInputMethodPicker;
+    static jmethodID jStartMonitoringGamepad;
+    static jmethodID jStopMonitoringGamepad;
     static jmethodID jUnlockProfile;
     static jmethodID jUnlockScreenOrientation;
     static jmethodID jUnregisterSurfaceTextureFrameListener;
     static jmethodID jVibrate1;
     static jmethodID jVibrateA;
 };
 
 class JavaDomKeyLocation : public AutoGlobalWrappedJavaObject {
--- a/widget/android/moz.build
+++ b/widget/android/moz.build
@@ -44,16 +44,17 @@ FAIL_ON_WARNINGS = True
 LIBRARY_NAME = 'widget_android'
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul'
 
 LOCAL_INCLUDES += [
     '/docshell/base',
+    '/dom/base',
     '/dom/system/android',
     '/netwerk/cache',
     '/widget/android/android',
     '/widget/shared',
     '/widget/xpwidgets',
 ]
 
 #DEFINES['DEBUG_WIDGETS'] = True
--- a/widget/android/nsAppShell.cpp
+++ b/widget/android/nsAppShell.cpp
@@ -34,16 +34,19 @@
 
 #include "AndroidBridge.h"
 #include "AndroidBridgeUtilities.h"
 #include <android/log.h>
 #include <pthread.h>
 #include <wchar.h>
 
 #include "mozilla/dom/ScreenOrientation.h"
+#ifdef MOZ_GAMEPAD
+#include "mozilla/dom/GamepadService.h"
+#endif
 
 #include "GeckoProfiler.h"
 #ifdef MOZ_ANDROID_HISTORY
 #include "nsNetUtil.h"
 #include "IHistory.h"
 #endif
 
 #ifdef MOZ_LOGGING
@@ -585,16 +588,59 @@ nsAppShell::ProcessNextNativeEvent(bool 
         break;
     }
 
     case AndroidGeckoEvent::TELEMETRY_HISTOGRAM_ADD:
         Telemetry::Accumulate(NS_ConvertUTF16toUTF8(curEvent->Characters()).get(),
                               curEvent->Count());
         break;
 
+    case AndroidGeckoEvent::GAMEPAD_ADDREMOVE: {
+#ifdef MOZ_GAMEPAD
+        nsRefPtr<mozilla::dom::GamepadService> svc =
+            mozilla::dom::GamepadService::GetService();
+        if (svc) {
+            if (curEvent->Action() == AndroidGeckoEvent::ACTION_GAMEPAD_ADDED) {
+                int svc_id = svc->AddGamepad("android",
+                                             mozilla::dom::StandardMapping,
+                                             mozilla::dom::kStandardGamepadButtons,
+                                             mozilla::dom::kStandardGamepadAxes);
+                mozilla::widget::android::GeckoAppShell::GamepadAdded(curEvent->ID(),
+                                                                      svc_id);
+            } else if (curEvent->Action() == AndroidGeckoEvent::ACTION_GAMEPAD_REMOVED) {
+                svc->RemoveGamepad(curEvent->ID());
+            }
+        }
+#endif
+        break;
+    }
+
+    case AndroidGeckoEvent::GAMEPAD_DATA: {
+#ifdef MOZ_GAMEPAD
+        nsRefPtr<mozilla::dom::GamepadService> svc =
+            mozilla::dom::GamepadService::GetService();
+        if (svc) {
+            int id = curEvent->ID();
+            if (curEvent->Action() == AndroidGeckoEvent::ACTION_GAMEPAD_BUTTON) {
+                 svc->NewButtonEvent(id, curEvent->GamepadButton(),
+                                     curEvent->GamepadButtonPressed(),
+                                     curEvent->GamepadButtonValue());
+            } else if (curEvent->Action() == AndroidGeckoEvent::ACTION_GAMEPAD_AXES) {
+                int valid = curEvent->Flags();
+                const nsTArray<float>& values = curEvent->GamepadValues();
+                for (unsigned i = 0; i < values.Length(); i++) {
+                    if (valid & (1<<i)) {
+                        svc->NewAxisMoveEvent(id, i, values[i]);
+                    }
+                }
+            }
+        }
+#endif
+        break;
+    }
     case AndroidGeckoEvent::NOOP:
         break;
 
     default:
         nsWindow::OnGlobalAndroidEvent(curEvent);
         break;
     }