1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.visualgamecontroller;
18 
19 import android.annotation.TargetApi;
20 import android.app.Activity;
21 import android.content.Context;
22 import android.hardware.input.InputManager;
23 import android.hardware.input.InputManager.InputDeviceListener;
24 import android.os.Build;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.util.Log;
28 import android.view.InputDevice;
29 import android.view.KeyEvent;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.view.WindowManager;
33 
34 import com.example.android.visualgamecontroller.util.SystemUiHider;
35 
36 import java.util.ArrayList;
37 
38 /**
39  * An example full-screen activity that shows and hides the system UI (i.e.
40  * status bar and navigation/system bar) with user interaction.
41  *
42  * @see SystemUiHider
43  */
44 public class FullscreenActivity extends Activity implements InputDeviceListener {
45     private static final String TAG = "FullscreenActivity";
46 
47     /**
48      * Whether or not the system UI should be auto-hidden after
49      * {@link #AUTO_HIDE_DELAY_MILLIS} milliseconds.
50      */
51     private static final boolean AUTO_HIDE = true;
52 
53     /**
54      * If {@link #AUTO_HIDE} is set, the number of milliseconds to wait after
55      * user interaction before hiding the system UI.
56      */
57     private static final int AUTO_HIDE_DELAY_MILLIS = 3000;
58 
59     /**
60      * If set, will toggle the system UI visibility upon interaction. Otherwise,
61      * will show the system UI visibility upon interaction.
62      */
63     private static final boolean TOGGLE_ON_CLICK = true;
64 
65     /**
66      * The flags to pass to {@link SystemUiHider#getInstance}.
67      */
68     private static final int HIDER_FLAGS = SystemUiHider.FLAG_HIDE_NAVIGATION;
69 
70     /**
71      * The instance of the {@link SystemUiHider} for this activity.
72      */
73     private SystemUiHider mSystemUiHider;
74 
75     private ControllerView mControllerView;
76 
77     public enum ButtonMapping {
78         BUTTON_A(KeyEvent.KEYCODE_BUTTON_A),
79         BUTTON_B(KeyEvent.KEYCODE_BUTTON_B),
80         BUTTON_X(KeyEvent.KEYCODE_BUTTON_X),
81         BUTTON_Y(KeyEvent.KEYCODE_BUTTON_Y),
82         BUTTON_L1(KeyEvent.KEYCODE_BUTTON_L1),
83         BUTTON_R1(KeyEvent.KEYCODE_BUTTON_R1),
84         BUTTON_L2(KeyEvent.KEYCODE_BUTTON_L2),
85         BUTTON_R2(KeyEvent.KEYCODE_BUTTON_R2),
86         BUTTON_SELECT(KeyEvent.KEYCODE_BUTTON_SELECT),
87         BUTTON_START(KeyEvent.KEYCODE_BUTTON_START),
88         BUTTON_THUMBL(KeyEvent.KEYCODE_BUTTON_THUMBL),
89         BUTTON_THUMBR(KeyEvent.KEYCODE_BUTTON_THUMBR),
90         BACK(KeyEvent.KEYCODE_BACK),
91         POWER(KeyEvent.KEYCODE_BUTTON_MODE);
92 
93         private final int mKeyCode;
94 
ButtonMapping(int keyCode)95         ButtonMapping(int keyCode) {
96             mKeyCode = keyCode;
97         }
98 
getKeycode()99         private int getKeycode() {
100             return mKeyCode;
101         }
102     }
103 
104     public enum AxesMapping {
105         AXIS_X(MotionEvent.AXIS_X),
106         AXIS_Y(MotionEvent.AXIS_Y),
107         AXIS_Z(MotionEvent.AXIS_Z),
108         AXIS_RZ(MotionEvent.AXIS_RZ),
109         AXIS_HAT_X(MotionEvent.AXIS_HAT_X),
110         AXIS_HAT_Y(MotionEvent.AXIS_HAT_Y),
111         AXIS_LTRIGGER(MotionEvent.AXIS_LTRIGGER),
112         AXIS_RTRIGGER(MotionEvent.AXIS_RTRIGGER),
113         AXIS_BRAKE(MotionEvent.AXIS_BRAKE),
114         AXIS_GAS(MotionEvent.AXIS_GAS);
115 
116         private final int mMotionEvent;
117 
AxesMapping(int motionEvent)118         AxesMapping(int motionEvent) {
119             mMotionEvent = motionEvent;
120         }
121 
getMotionEvent()122         private int getMotionEvent() {
123             return mMotionEvent;
124         }
125     }
126 
127     private int[] mButtons = new int[ButtonMapping.values().length];
128     private float[] mAxes = new float[AxesMapping.values().length];
129     private InputManager mInputManager;
130     private ArrayList<Integer> mConnectedDevices = new ArrayList<Integer>();
131     private int mCurrentDeviceId = -1;
132 
133     @Override
onCreate(Bundle savedInstanceState)134     protected void onCreate(Bundle savedInstanceState) {
135         super.onCreate(savedInstanceState);
136         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
137         setContentView(R.layout.activity_fullscreen);
138 
139         final View controlsView = findViewById(R.id.fullscreen_content_controls);
140         final View contentView = findViewById(R.id.fullscreen_content);
141 
142         mControllerView = (ControllerView) findViewById(R.id.controller);
143         for (int i = 0; i < mButtons.length; i++) {
144             mButtons[i] = 0;
145         }
146         for (int i = 0; i < mAxes.length; i++) {
147             mAxes[i] = 0.0f;
148         }
149         mControllerView.setButtonsAxes(mButtons, mAxes);
150 
151         // Set up an instance of SystemUiHider to control the system UI for
152         // this activity.
153         mSystemUiHider = SystemUiHider.getInstance(this, contentView, HIDER_FLAGS);
154         mSystemUiHider.setup();
155         mSystemUiHider
156                 .setOnVisibilityChangeListener(new SystemUiHider.OnVisibilityChangeListener() {
157                     // Cached values.
158                     int mControlsHeight;
159                     int mShortAnimTime;
160 
161                     @Override
162                     @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
163                     public void onVisibilityChange(boolean visible) {
164                         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
165                             // If the ViewPropertyAnimator API is available
166                             // (Honeycomb MR2 and later), use it to animate the
167                             // in-layout UI controls at the bottom of the
168                             // screen.
169                             if (mControlsHeight == 0) {
170                                 mControlsHeight = controlsView.getHeight();
171                             }
172                             if (mShortAnimTime == 0) {
173                                 mShortAnimTime = getResources().getInteger(
174                                         android.R.integer.config_shortAnimTime);
175                             }
176                             controlsView.animate()
177                                     .translationY(visible ? 0 : mControlsHeight)
178                                     .setDuration(mShortAnimTime);
179                         } else {
180                             // If the ViewPropertyAnimator APIs aren't
181                             // available, simply show or hide the in-layout UI
182                             // controls.
183                             controlsView.setVisibility(visible ? View.VISIBLE : View.GONE);
184                         }
185 
186                         if (visible && AUTO_HIDE) {
187                             // Schedule a hide().
188                             delayedHide(AUTO_HIDE_DELAY_MILLIS);
189                         }
190                     }
191                 });
192 
193         // Set up the user interaction to manually show or hide the system UI.
194         contentView.setOnClickListener(new View.OnClickListener() {
195             @Override
196             public void onClick(View view) {
197                 if (TOGGLE_ON_CLICK) {
198                     mSystemUiHider.toggle();
199                 } else {
200                     mSystemUiHider.show();
201                 }
202             }
203         });
204 
205         mInputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
206         checkGameControllers();
207     }
208 
209     /**
210      * Check for any game controllers that are connected already.
211      */
checkGameControllers()212     private void checkGameControllers() {
213         Log.d(TAG, "checkGameControllers");
214         int[] deviceIds = mInputManager.getInputDeviceIds();
215         for (int deviceId : deviceIds) {
216             InputDevice dev = InputDevice.getDevice(deviceId);
217             int sources = dev.getSources();
218 
219             // Verify that the device has gamepad buttons, control sticks, or
220             // both.
221             if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
222                     || ((sources & InputDevice.SOURCE_JOYSTICK)
223                         == InputDevice.SOURCE_JOYSTICK)) {
224                 // This device is a game controller. Store its device ID.
225                 if (!mConnectedDevices.contains(deviceId)) {
226                     mConnectedDevices.add(deviceId);
227                     if (mCurrentDeviceId == -1) {
228                         mCurrentDeviceId = deviceId;
229                         mControllerView.setCurrentControllerNumber(dev.getControllerNumber());
230                         mControllerView.invalidate();
231                     }
232                 }
233             }
234         }
235     }
236 
237     @Override
onPostCreate(Bundle savedInstanceState)238     protected void onPostCreate(Bundle savedInstanceState) {
239         super.onPostCreate(savedInstanceState);
240 
241         // Trigger the initial hide() shortly after the activity has been
242         // created, to briefly hint to the user that UI controls
243         // are available.
244         delayedHide(100);
245     }
246 
247     @Override
onResume()248     protected void onResume() {
249         super.onResume();
250         mInputManager.registerInputDeviceListener(this, null);
251     }
252 
253     @Override
onPause()254     protected void onPause() {
255         super.onPause();
256         mInputManager.unregisterInputDeviceListener(this);
257     }
258 
259     /**
260      * Touch listener to use for in-layout UI controls to delay hiding the
261      * system UI. This is to prevent the jarring behavior of controls going away
262      * while interacting with activity UI.
263      */
264     View.OnTouchListener mDelayHideTouchListener = new View.OnTouchListener() {
265         @Override
266         public boolean onTouch(View view, MotionEvent motionEvent) {
267             if (AUTO_HIDE) {
268                 delayedHide(AUTO_HIDE_DELAY_MILLIS);
269             }
270             return false;
271         }
272     };
273 
274     Handler mHideHandler = new Handler();
275     Runnable mHideRunnable = new Runnable() {
276         @Override
277         public void run() {
278             mSystemUiHider.hide();
279         }
280     };
281 
282     /**
283      * Schedules a call to hide() in [delay] milliseconds, canceling any
284      * previously scheduled calls.
285      */
delayedHide(int delayMillis)286     private void delayedHide(int delayMillis) {
287         mHideHandler.removeCallbacks(mHideRunnable);
288         mHideHandler.postDelayed(mHideRunnable, delayMillis);
289     }
290 
291     /*
292      * (non-Javadoc)
293      * @see android.app.Activity#onGenericMotionEvent(android.view.MotionEvent)
294      */
295     @Override
onGenericMotionEvent(final MotionEvent ev)296     public boolean onGenericMotionEvent(final MotionEvent ev) {
297         // Log.d(TAG, "onGenericMotionEvent: " + ev);
298         InputDevice device = ev.getDevice();
299         // Only care about game controllers.
300         if (device != null && device.getId() == mCurrentDeviceId) {
301             if (isGamepad(device)) {
302                 for (AxesMapping axesMapping : AxesMapping.values()) {
303                     mAxes[axesMapping.ordinal()] = getCenteredAxis(ev, device,
304                             axesMapping.getMotionEvent());
305                 }
306                 mControllerView.invalidate();
307                 return true;
308             }
309         }
310         return super.onGenericMotionEvent(ev);
311     }
312 
313     /**
314      * Get centered position for axis input by considering flat area.
315      *
316      * @param event
317      * @param device
318      * @param axis
319      * @return
320      */
getCenteredAxis(MotionEvent event, InputDevice device, int axis)321     private float getCenteredAxis(MotionEvent event, InputDevice device, int axis) {
322         InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource());
323 
324         // A joystick at rest does not always report an absolute position of
325         // (0,0). Use the getFlat() method to determine the range of values
326         // bounding the joystick axis center.
327         if (range != null) {
328             float flat = range.getFlat();
329             float value = event.getAxisValue(axis);
330 
331             // Ignore axis values that are within the 'flat' region of the
332             // joystick axis center.
333             if (Math.abs(value) > flat) {
334                 return value;
335             }
336         }
337         return 0;
338     }
339 
340     /*
341      * (non-Javadoc)
342      * @see android.support.v4.app.FragmentActivity#onKeyDown(int,
343      * android.view.KeyEvent)
344      */
345     @Override
onKeyDown(final int keyCode, KeyEvent ev)346     public boolean onKeyDown(final int keyCode, KeyEvent ev) {
347         // Log.d(TAG, "onKeyDown: " + ev);
348         InputDevice device = ev.getDevice();
349         // Only care about game controllers.
350         if (device != null && device.getId() == mCurrentDeviceId) {
351             if (isGamepad(device)) {
352                 int index = getButtonMappingIndex(keyCode);
353                 if (index >= 0) {
354                     mButtons[index] = 1;
355                     mControllerView.invalidate();
356                 }
357                 return true;
358             }
359         }
360         return super.onKeyDown(keyCode, ev);
361     }
362 
363     /*
364      * (non-Javadoc)
365      * @see android.app.Activity#onKeyUp(int, android.view.KeyEvent)
366      */
367     @Override
onKeyUp(final int keyCode, KeyEvent ev)368     public boolean onKeyUp(final int keyCode, KeyEvent ev) {
369         // Log.d(TAG, "onKeyUp: " + ev);
370         InputDevice device = ev.getDevice();
371         // Only care about game controllers.
372         if (device != null && device.getId() == mCurrentDeviceId) {
373             if (isGamepad(device)) {
374                 int index = getButtonMappingIndex(keyCode);
375                 if (index >= 0) {
376                     mButtons[index] = 0;
377                     mControllerView.invalidate();
378                 }
379                 return true;
380             }
381         }
382         return super.onKeyUp(keyCode, ev);
383     }
384 
385     /**
386      * Utility method to determine if input device is a gamepad.
387      *
388      * @param device
389      * @return
390      */
isGamepad(InputDevice device)391     private boolean isGamepad(InputDevice device) {
392         if ((device.getSources() &
393                 InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD
394                 || (device.getSources() &
395                 InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_JOYSTICK) {
396             return true;
397         }
398         return false;
399     }
400 
401     /**
402      * Get the array index for the key code.
403      *
404      * @param keyCode
405      * @return
406      */
getButtonMappingIndex(int keyCode)407     private int getButtonMappingIndex(int keyCode) {
408         for (ButtonMapping buttonMapping : ButtonMapping.values()) {
409             if (buttonMapping.getKeycode() == keyCode) {
410                 return buttonMapping.ordinal();
411             }
412         }
413         return -1;
414     }
415 
416     /*
417      * (non-Javadoc)
418      * @see
419      * android.hardware.input.InputManager.InputDeviceListener#onInputDeviceAdded
420      * (int)
421      */
422     @Override
onInputDeviceAdded(int deviceId)423     public void onInputDeviceAdded(int deviceId) {
424         Log.d(TAG, "onInputDeviceAdded: " + deviceId);
425         if (!mConnectedDevices.contains(deviceId)) {
426             mConnectedDevices.add(new Integer(deviceId));
427         }
428         if (mCurrentDeviceId == -1) {
429             mCurrentDeviceId = deviceId;
430             InputDevice dev = InputDevice.getDevice(mCurrentDeviceId);
431             if (dev != null) {
432                 mControllerView.setCurrentControllerNumber(dev.getControllerNumber());
433                 mControllerView.invalidate();
434             }
435         }
436     }
437 
438     /*
439      * (non-Javadoc)
440      * @see
441      * android.hardware.input.InputManager.InputDeviceListener#onInputDeviceRemoved
442      * (int)
443      */
444     @Override
onInputDeviceRemoved(int deviceId)445     public void onInputDeviceRemoved(int deviceId) {
446         Log.d(TAG, "onInputDeviceRemoved: " + deviceId);
447         mConnectedDevices.remove(new Integer(deviceId));
448         if (mCurrentDeviceId == deviceId) {
449             mCurrentDeviceId = -1;
450         }
451         if (mConnectedDevices.size() == 0) {
452             mControllerView.setCurrentControllerNumber(-1);
453             mControllerView.invalidate();
454         } else {
455             mCurrentDeviceId = mConnectedDevices.get(0);
456             InputDevice dev = InputDevice.getDevice(mCurrentDeviceId);
457             if (dev != null) {
458                 mControllerView.setCurrentControllerNumber(dev.getControllerNumber());
459                 mControllerView.invalidate();
460             }
461         }
462     }
463 
464     /*
465      * (non-Javadoc)
466      * @see
467      * android.hardware.input.InputManager.InputDeviceListener#onInputDeviceChanged
468      * (int)
469      */
470     @Override
onInputDeviceChanged(int deviceId)471     public void onInputDeviceChanged(int deviceId) {
472         Log.d(TAG, "onInputDeviceChanged: " + deviceId);
473         mControllerView.invalidate();
474     }
475 
476 }
477