bug 852935 - Add Android gamepad backend. r=snorp, rs=smaug
authorTed Mielczarek <ted@mielczarek.org>
Tue, 22 Apr 2014 10:53:48 -0400
changeset 200302 885b5b8fd7d95a83a98151b5d68d65fc2a164980
parent 200301 74ef5120ae2f590d8b766806025610f200ca3c8a
child 200303 289e653a7061a773972b5633e6f53718514012c3
push id3741
push userasasaki@mozilla.com
push dateMon, 21 Jul 2014 20:25:18 +0000
treeherdermozilla-beta@4d6f46f5af68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp, smaug
bugs852935
milestone32.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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;
     }