• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1page.title=Supporting Controllers Across Android Versions
2trainingnavtop=true
3
4@jd:body
5
6<!-- This is the training bar -->
7<div id="tb-wrapper">
8<div id="tb">
9
10<h2>This lesson teaches you to</h2>
11<ol>
12  <li><a href="#prepare">Prepare to Abstract APIs for Game Controller
13Suppport</a></li>
14  <li><a href="#abstraction">Add an Interface for Backward Compatibility</a></li>
15  <li><a href="#newer">Implement the Interface on Android 4.1 and Higher</a></li>
16  <li><a href="#older">Implement the Interface on Android 3.1 up to Android
174.0</a></li>
18  <li><a href="#using">Use the Version-Specific Implementations</a></li>
19</ol>
20
21<h2>Try it out</h2>
22<div class="download-box">
23  <a href="http://developer.android.com/shareables/training/ControllerSample.zip"
24class="button">Download the sample</a>
25  <p class="filename">ControllerSample.zip</p>
26</div>
27
28</div>
29</div>
30
31<p>If you are supporting game controllers in your game, it's your responsibility
32to make sure that your game responds to controllers consistently across devices
33running on different versions of Android. This lets your game reach a wider
34audience, and your players can enjoy a seamless gameplay experience with
35their controllers even when they switch or upgrade their Android devices.</p>
36
37<p>This lesson demonstrates how to use APIs available in Android 4.1 and higher
38in a backward compatible way, enabling your game to support the following
39features on devices running Android 3.1 and higher:</p>
40<ul>
41<li>The game can detect if a new game controller is added, changed, or removed.</li>
42<li>The game can query the capabilities of a game controller.</li>
43<li>The game can recognize incoming motion events from a game controller.</li>
44</ul>
45
46<p>The examples in this lesson are based on the reference implementation
47provided by the sample {@code ControllerSample.zip} available for download
48above. This sample shows how to implement the {@code InputManagerCompat}
49interface to support different versions of Android. To compile the sample, you
50must use Android 4.1 (API level 16) or higher. Once compiled, the sample app
51runs on any device running Android 3.1 (API level 12) or higher as the build
52target.
53</p>
54
55<h2 id="prepare">Prepare to Abstract APIs for Game Controller Support</h2>
56<p>Suppose you want to be able to determine if a game controller's connection
57status has changed on devices running on Android 3.1 (API level 12). However,
58the APIs are only available in Android 4.1 (API level 16) and higher, so you
59need to provide an implementation that supports Android 4.1 and higher while
60providing a fallback mechanism that supports Android 3.1 up to Android 4.0.</p>
61
62<p>To help you determine which features require such a fallback mechanism for
63    older versions, table 1 lists the differences in game controller support
64    between Android 3.1 (API level 12) and 4.1 (API level
65    16).</p>
66
67<p class="table-caption" id="game-controller-support-table">
68<strong>Table 1.</strong> APIs for game controller support across
69different Android versions.
70</p>
71
72<table>
73<tbody>
74<tr>
75<th>Controller Information</th>
76<th>Controller API</th>
77<th>API level 12</th>
78<th>API level 16</th>
79</tr>
80
81<tr>
82<td rowspan="5">Device Identification</td>
83<td>{@link android.hardware.input.InputManager#getInputDeviceIds()}</td>
84<td style="text-align: center;"><big>&nbsp;</big></td>
85<td style="text-align: center;"><big>&bull;</big></td>
86</tr>
87
88<tr>
89<td>{@link android.hardware.input.InputManager#getInputDevice(int)
90getInputDevice()}</td>
91<td style="text-align: center;"><big>&nbsp;</big></td>
92<td style="text-align: center;"><big>&bull;</big></td>
93</tr>
94
95<tr>
96<td>{@link android.view.InputDevice#getVibrator()}</td>
97<td style="text-align: center;"><big>&nbsp;</big></td>
98<td style="text-align: center;"><big>&bull;</big></td>
99</tr>
100
101<td>{@link android.view.InputDevice#SOURCE_JOYSTICK}</td>
102<td style="text-align: center;"><big>&bull;</big></td>
103<td style="text-align: center;"><big>&bull;</big></td>
104</tr>
105
106<tr>
107<td>{@link android.view.InputDevice#SOURCE_GAMEPAD}</td>
108<td style="text-align: center;"><big>&bull;</big></td>
109<td style="text-align: center;"><big>&bull;</big></td>
110</tr>
111
112<tr>
113<td rowspan="3">Connection Status</td>
114<td>{@link android.hardware.input.InputManager.InputDeviceListener#onInputDeviceAdded(int) onInputDeviceAdded()}</td>
115<td style="text-align: center;">&nbsp;</td>
116<td style="text-align: center;"><big>&bull;</big></td>
117</tr>
118
119<tr>
120<td>{@link android.hardware.input.InputManager.InputDeviceListener#onInputDeviceChanged(int) onInputDeviceChanged()}</td>
121<td style="text-align: center;">&nbsp;</td>
122<td style="text-align: center;"><big>&bull;</big></td>
123</tr>
124
125<tr>
126<td>{@link android.hardware.input.InputManager.InputDeviceListener#onInputDeviceRemoved(int) onInputDeviceRemoved()}</td>
127<td style="text-align: center;">&nbsp;</td>
128<td style="text-align: center;"><big>&bull;</big></td>
129</tr>
130
131<tr>
132<td rowspan="4">Input Event Identification</td>
133<td>D-pad press (
134{@link android.view.KeyEvent#KEYCODE_DPAD_UP},
135{@link android.view.KeyEvent#KEYCODE_DPAD_DOWN},
136{@link android.view.KeyEvent#KEYCODE_DPAD_LEFT},
137{@link android.view.KeyEvent#KEYCODE_DPAD_RIGHT},
138{@link android.view.KeyEvent#KEYCODE_DPAD_CENTER})</td>
139<td style="text-align: center;"><big>&bull;</big></td>
140<td style="text-align: center;"><big>&bull;</big></td>
141</tr>
142
143<tr>
144<td>Gamepad button press (
145{@link android.view.KeyEvent#KEYCODE_BUTTON_A BUTTON_A},
146{@link android.view.KeyEvent#KEYCODE_BUTTON_B BUTTON_B},
147{@link android.view.KeyEvent#KEYCODE_BUTTON_THUMBL BUTTON_THUMBL},
148{@link android.view.KeyEvent#KEYCODE_BUTTON_THUMBR BUTTON_THUMBR},
149{@link android.view.KeyEvent#KEYCODE_BUTTON_SELECT BUTTON_SELECT},
150{@link android.view.KeyEvent#KEYCODE_BUTTON_START BUTTON_START},
151{@link android.view.KeyEvent#KEYCODE_BUTTON_R1 BUTTON_R1},
152{@link android.view.KeyEvent#KEYCODE_BUTTON_L1 BUTTON_L1},
153{@link android.view.KeyEvent#KEYCODE_BUTTON_R2 BUTTON_R2},
154{@link android.view.KeyEvent#KEYCODE_BUTTON_L2 BUTTON_L2})</td>
155<td style="text-align: center;"><big>&bull;</big></td>
156<td style="text-align: center;"><big>&bull;</big></td>
157</tr>
158
159<tr>
160<td>Joystick and hat switch movement (
161{@link android.view.MotionEvent#AXIS_X},
162{@link android.view.MotionEvent#AXIS_Y},
163{@link android.view.MotionEvent#AXIS_Z},
164{@link android.view.MotionEvent#AXIS_RZ},
165{@link android.view.MotionEvent#AXIS_HAT_X},
166{@link android.view.MotionEvent#AXIS_HAT_Y})</td>
167<td style="text-align: center;"><big>&bull;</big></td>
168<td style="text-align: center;"><big>&bull;</big></td>
169</tr>
170
171<tr>
172<td>Analog trigger press (
173{@link android.view.MotionEvent#AXIS_LTRIGGER},
174{@link android.view.MotionEvent#AXIS_RTRIGGER})</td>
175<td style="text-align: center;"><big>&bull;</big></td>
176<td style="text-align: center;"><big>&bull;</big></td>
177</tr>
178
179</tbody>
180</table>
181
182<p>You can use abstraction to build version-aware game controller support that
183works across platforms. This approach involves the following steps:</p>
184<ol>
185<li>Define an intermediary Java interface that abstracts the implementation of
186the game controller features required by your game.</li>
187<li>Create a proxy implementation of your interface that uses APIs in Android
1884.1 and higher.</li>
189<li>Create a custom implementation of your interface that uses APIs available
190between Android 3.1 up to Android 4.0.</li>
191<li>Create the logic for switching between these implementations at runtime,
192and begin using the interface in your game.</li>
193</ol>
194
195<p>For an overview of how abstraction can be used to ensure that applications
196can work in a backward compatible way across different versions of Android, see
197<a href="{@docRoot}training/backward-compatible-ui/index.html">Creating
198Backward-Compatible UIs</a>.
199</p>
200
201<h2 id="abstraction">Add an Interface for Backward Compatibility</h2>
202
203<p>To provide backward compatibility, you can create a custom interface then
204add version-specific implementations. One advantage of this approach is that it
205lets you mirror the public interfaces on Android 4.1 (API level 16) that
206support game controllers.</p>
207<pre>
208// The InputManagerCompat interface is a reference example.
209// The full code is provided in the ControllerSample.zip sample.
210public interface InputManagerCompat {
211    ...
212    public InputDevice getInputDevice(int id);
213    public int[] getInputDeviceIds();
214
215    public void registerInputDeviceListener(
216            InputManagerCompat.InputDeviceListener listener,
217            Handler handler);
218    public void unregisterInputDeviceListener(
219            InputManagerCompat.InputDeviceListener listener);
220
221    public void onGenericMotionEvent(MotionEvent event);
222
223    public void onPause();
224    public void onResume();
225
226    public interface InputDeviceListener {
227        void onInputDeviceAdded(int deviceId);
228        void onInputDeviceChanged(int deviceId);
229        void onInputDeviceRemoved(int deviceId);
230    }
231    ...
232}
233</pre>
234<p>The {@code InputManagerCompat} interface provides the following methods:</p>
235<dl>
236<dt>{@code getInputDevice()}</dt>
237<dd>Mirrors {@link android.hardware.input.InputManager#getInputDevice(int)
238getInputDevice()}. Obtains the {@link android.view.InputDevice}
239object that represents the capabilities of a game controller.</dd>
240<dt>{@code getInputDeviceIds()}</dt>
241<dd>Mirrors {@link android.hardware.input.InputManager#getInputDeviceIds()
242getInputDeviceIds()}. Returns an array of integers, each of
243which is an ID for a different input device. This is useful if you're building
244a game that supports multiple players and you want to detect how many
245controllers are connected.</dd>
246<dt>{@code registerInputDeviceListener()}</dt>
247<dd>Mirrors {@link android.hardware.input.InputManager#registerInputDeviceListener(android.hardware.input.InputManager.InputDeviceListener, android.os.Handler)
248registerInputDeviceListener()}. Lets you register to be informed when a new
249device is added, changed, or removed.</dd>
250<dt>{@code unregisterInputDeviceListener()}</dt>
251<dd>Mirrors {@link android.hardware.input.InputManager#unregisterInputDeviceListener(android.hardware.input.InputManager.InputDeviceListener) unregisterInputDeviceListener()}.
252Unregisters an input device listener.</dd>
253<dt>{@code onGenericMotionEvent()}</dt>
254<dd>Mirrors {@link android.view.View#onGenericMotionEvent(android.view.MotionEvent)
255onGenericMotionEvent()}. Lets your game intercept and handle
256{@link android.view.MotionEvent} objects and axis values that represent events
257such as joystick movements and analog trigger presses.</dd>
258<dt>{@code onPause()}</dt>
259<dd>Stops polling for game controller events when the
260main activity is paused, or when the game no longer has focus.</dd>
261<dt>{@code onResume()}</dt>
262<dd>Starts polling for game controller events when the
263main activity is resumed, or when the game is started and runs in the
264foreground.</dd>
265<dt>{@code InputDeviceListener}</dt>
266<dd>Mirrors the {@link android.hardware.input.InputManager.InputDeviceListener}
267interface. Lets your game know when a game controller has been added, changed, or
268removed.</dd>
269</dl>
270<p>Next, create implementations for {@code InputManagerCompat} that work
271across different platform versions. If your game is running on Android 4.1 or
272higher and calls an {@code InputManagerCompat} method, the proxy implementation
273calls the equivalent method in {@link android.hardware.input.InputManager}.
274However, if your game is running on Android 3.1 up to Android 4.0, the custom implementation
275processes calls to {@code InputManagerCompat} methods by using
276only APIs introduced no later than Android 3.1. Regardless of which
277version-specific implementation is used at runtime, the implementation passes
278the call results back transparently to the game.</p>
279
280<img src="{@docRoot}images/training/backward-compatible-inputmanager.png" alt=""
281id="figure1" />
282<p class="img-caption">
283  <strong>Figure 1.</strong> Class diagram of interface and version-specific
284implementations.
285</p>
286
287<h2 id="newer">Implement the Interface on Android 4.1 and Higher</h2>
288<p>{@code InputManagerCompatV16} is an implementation of the
289{@code InputManagerCompat} interface that proxies method calls to an
290actual {@link android.hardware.input.InputManager} and {@link
291android.hardware.input.InputManager.InputDeviceListener}. The
292{@link android.hardware.input.InputManager} is obtained from the system
293{@link android.content.Context}.</p>
294
295<pre>
296// The InputManagerCompatV16 class is a reference implementation.
297// The full code is provided in the ControllerSample.zip sample.
298public class InputManagerV16 implements InputManagerCompat {
299
300    private final InputManager mInputManager;
301    private final Map<InputManagerCompat.InputDeviceListener,
302            V16InputDeviceListener> mListeners;
303
304    public InputManagerV16(Context context) {
305        mInputManager = (InputManager)
306                context.getSystemService(Context.INPUT_SERVICE);
307        mListeners = new HashMap<InputManagerCompat.InputDeviceListener,
308                V16InputDeviceListener>();
309    }
310
311    &#64;Override
312    public InputDevice getInputDevice(int id) {
313        return mInputManager.getInputDevice(id);
314    }
315
316    &#64;Override
317    public int[] getInputDeviceIds() {
318        return mInputManager.getInputDeviceIds();
319    }
320
321    static class V16InputDeviceListener implements
322            InputManager.InputDeviceListener {
323        final InputManagerCompat.InputDeviceListener mIDL;
324
325        public V16InputDeviceListener(InputDeviceListener idl) {
326            mIDL = idl;
327        }
328
329        &#64;Override
330        public void onInputDeviceAdded(int deviceId) {
331            mIDL.onInputDeviceAdded(deviceId);
332        }
333
334        // Do the same for device change and removal
335        ...
336    }
337
338    &#64;Override
339    public void registerInputDeviceListener(InputDeviceListener listener,
340            Handler handler) {
341        V16InputDeviceListener v16Listener = new
342                V16InputDeviceListener(listener);
343        mInputManager.registerInputDeviceListener(v16Listener, handler);
344        mListeners.put(listener, v16Listener);
345    }
346
347    // Do the same for unregistering an input device listener
348    ...
349
350    &#64;Override
351    public void onGenericMotionEvent(MotionEvent event) {
352        // unused in V16
353    }
354
355    &#64;Override
356    public void onPause() {
357        // unused in V16
358    }
359
360    &#64;Override
361    public void onResume() {
362        // unused in V16
363    }
364
365}
366</pre>
367
368<h2 id="older">Implementing the Interface on Android 3.1 up to Android 4.0</h2>
369
370<p>To create an implementation of {@code
371InputManagerCompat} that supports Android 3.1 up to Android 4.0, you can use
372the following objects:
373<ul>
374<li>A {@link android.util.SparseArray} of device IDs to track the
375game controllers that are connected to the device.</li>
376<li>A {@link android.os.Handler} to process device events. When an app is started
377or resumed, the {@link android.os.Handler} receives a message to start polling
378for game controller disconnection. The {@link android.os.Handler} will start a
379loop to check each known connected game controller and see if a device ID is
380returned. A {@code null} return value indicates that the game controller is
381disconnected. The {@link android.os.Handler} stops polling when the app is
382paused.</li>
383<li>A {@link java.util.Map} of {@code InputManagerCompat.InputDeviceListener}
384objects. You will use the listeners to update the connection status of tracked
385game controllers.</li>
386</ul>
387
388<pre>
389// The InputManagerCompatV9 class is a reference implementation.
390// The full code is provided in the ControllerSample.zip sample.
391public class InputManagerV9 implements InputManagerCompat {
392    private final SparseArray<long[]> mDevices;
393    private final Map<InputDeviceListener, Handler> mListeners;
394    private final Handler mDefaultHandler;
395396
397    public InputManagerV9() {
398        mDevices = new SparseArray<long[]>();
399        mListeners = new HashMap<InputDeviceListener, Handler>();
400        mDefaultHandler = new PollingMessageHandler(this);
401    }
402}
403</pre>
404
405<p>Implement a {@code PollingMessageHandler} object that extends
406{@link android.os.Handler}, and override the
407{@link android.os.Handler#handleMessage(android.os.Message) handleMessage()}
408method. This method checks if an attached game controller has been
409disconnected and notifies registered listeners.</p>
410
411<pre>
412private static class PollingMessageHandler extends Handler {
413    private final WeakReference<InputManagerV9> mInputManager;
414
415    PollingMessageHandler(InputManagerV9 im) {
416        mInputManager = new WeakReference<InputManagerV9>(im);
417    }
418
419    &#64;Override
420    public void handleMessage(Message msg) {
421        super.handleMessage(msg);
422        switch (msg.what) {
423            case MESSAGE_TEST_FOR_DISCONNECT:
424                InputManagerV9 imv = mInputManager.get();
425                if (null != imv) {
426                    long time = SystemClock.elapsedRealtime();
427                    int size = imv.mDevices.size();
428                    for (int i = 0; i &lt; size; i++) {
429                        long[] lastContact = imv.mDevices.valueAt(i);
430                        if (null != lastContact) {
431                            if (time - lastContact[0] > CHECK_ELAPSED_TIME) {
432                                // check to see if the device has been
433                                // disconnected
434                                int id = imv.mDevices.keyAt(i);
435                                if (null == InputDevice.getDevice(id)) {
436                                    // Notify the registered listeners
437                                    // that the game controller is disconnected
438                                    ...
439                                    imv.mDevices.remove(id);
440                                } else {
441                                    lastContact[0] = time;
442                                }
443                            }
444                        }
445                    }
446                    sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT,
447                            CHECK_ELAPSED_TIME);
448                }
449                break;
450        }
451    }
452}
453</pre>
454
455<p>To start and stop polling for game controller disconnection, override
456these methods:</p>
457<pre>
458private static final int MESSAGE_TEST_FOR_DISCONNECT = 101;
459private static final long CHECK_ELAPSED_TIME = 3000L;
460
461&#64;Override
462public void onPause() {
463    mDefaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT);
464}
465
466&#64;Override
467public void onResume() {
468    mDefaultHandler.sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT,
469            CHECK_ELAPSED_TIME);
470}
471</pre>
472
473<p>To detect that an input device has been added, override the
474{@code onGenericMotionEvent()} method. When the system reports a motion event,
475check if this event came from a device ID that is already tracked, or from a
476new device ID. If the device ID is new, notify registered listeners.</p>
477
478<pre>
479&#64;Override
480public void onGenericMotionEvent(MotionEvent event) {
481    // detect new devices
482    int id = event.getDeviceId();
483    long[] timeArray = mDevices.get(id);
484    if (null == timeArray) {
485        // Notify the registered listeners that a game controller is added
486        ...
487        timeArray = new long[1];
488        mDevices.put(id, timeArray);
489    }
490    long time = SystemClock.elapsedRealtime();
491    timeArray[0] = time;
492}
493</pre>
494
495<p>Notification of listeners is implemented by using the
496{@link android.os.Handler} object to send a {@code DeviceEvent}
497{@link java.lang.Runnable} object to the message queue. The {@code DeviceEvent}
498contains a reference to an {@code InputManagerCompat.InputDeviceListener}. When
499the {@code DeviceEvent} runs, the appropriate callback method of the listener
500is called to signal if the game controller was added, changed, or removed.
501</p>
502
503<pre>
504&#64;Override
505public void registerInputDeviceListener(InputDeviceListener listener,
506        Handler handler) {
507    mListeners.remove(listener);
508    if (handler == null) {
509        handler = mDefaultHandler;
510    }
511    mListeners.put(listener, handler);
512}
513
514&#64;Override
515public void unregisterInputDeviceListener(InputDeviceListener listener) {
516    mListeners.remove(listener);
517}
518
519private void notifyListeners(int why, int deviceId) {
520    // the state of some device has changed
521    if (!mListeners.isEmpty()) {
522        for (InputDeviceListener listener : mListeners.keySet()) {
523            Handler handler = mListeners.get(listener);
524            DeviceEvent odc = DeviceEvent.getDeviceEvent(why, deviceId,
525                    listener);
526            handler.post(odc);
527        }
528    }
529}
530
531private static class DeviceEvent implements Runnable {
532    private int mMessageType;
533    private int mId;
534    private InputDeviceListener mListener;
535    private static Queue<DeviceEvent> sObjectQueue =
536            new ArrayDeque<DeviceEvent>();
537    ...
538
539    static DeviceEvent getDeviceEvent(int messageType, int id,
540            InputDeviceListener listener) {
541        DeviceEvent curChanged = sObjectQueue.poll();
542        if (null == curChanged) {
543            curChanged = new DeviceEvent();
544        }
545        curChanged.mMessageType = messageType;
546        curChanged.mId = id;
547        curChanged.mListener = listener;
548        return curChanged;
549    }
550
551    &#64;Override
552    public void run() {
553        switch (mMessageType) {
554            case ON_DEVICE_ADDED:
555                mListener.onInputDeviceAdded(mId);
556                break;
557            case ON_DEVICE_CHANGED:
558                mListener.onInputDeviceChanged(mId);
559                break;
560            case ON_DEVICE_REMOVED:
561                mListener.onInputDeviceRemoved(mId);
562                break;
563            default:
564                // Handle unknown message type
565                ...
566                break;
567        }
568        // Put this runnable back in the queue
569        sObjectQueue.offer(this);
570    }
571}
572</pre>
573
574<p>You now have two implementations of {@code InputManagerCompat}: one that
575works on devices running Android 4.1 and higher, and another
576that works on devices running Android 3.1 up to Android 4.0.</p>
577
578<h2 id="using">Use the Version-Specific Implementation</h2>
579<p>The version-specific switching logic is implemented in a class that acts as
580a <a href="http://en.wikipedia.org/wiki/Factory_(software_concept)"
581class="external-link" target="_blank">factory</a>.</p>
582<pre>
583public static class Factory {
584    public static InputManagerCompat getInputManager(Context context) {
585        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
586            return new InputManagerV16(context);
587        } else {
588            return new InputManagerV9();
589        }
590    }
591}
592</pre>
593<p>Now you can simply instantiate an {@code InputManagerCompat} object and
594register an {@code InputManagerCompat.InputDeviceListener} in your main
595{@link android.view.View}. Because of the version-switching logic you set
596up, your game automatically uses the implementation that's appropriate for the
597version of Android the device is running.</p>
598<pre>
599public class GameView extends View implements InputDeviceListener {
600    private InputManagerCompat mInputManager;
601    ...
602
603    public GameView(Context context, AttributeSet attrs) {
604        mInputManager =
605                InputManagerCompat.Factory.getInputManager(this.getContext());
606        mInputManager.registerInputDeviceListener(this, null);
607        ...
608    }
609}
610</pre>
611<p>Next, override the
612{@link android.view.View#onGenericMotionEvent(android.view.MotionEvent)
613onGenericMotionEvent()} method in your main view, as described in
614<a href="controller-input.html#analog">Handle a MotionEvent from a Game
615Controller</a>. Your game should now be able to process game controller events
616consistently on devices running Android 3.1 (API level 12) and higher.
617<p>
618<pre>
619&#64;Override
620public boolean onGenericMotionEvent(MotionEvent event) {
621    mInputManager.onGenericMotionEvent(event);
622
623    // Handle analog input from the controller as normal
624    ...
625    return super.onGenericMotionEvent(event);
626}
627</pre>
628<p>You can find a complete implementation of this compatibility code in the
629{@code GameView} class provided in the sample {@code ControllerSample.zip}
630available for download above.</p>
631