1 package org.libsdl.app;
2 
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.util.ArrayList;
6 import java.util.Arrays;
7 import java.util.Collections;
8 import java.util.Comparator;
9 import java.util.List;
10 import java.lang.reflect.Method;
11 
12 import android.app.*;
13 import android.content.*;
14 import android.text.InputType;
15 import android.view.*;
16 import android.view.inputmethod.BaseInputConnection;
17 import android.view.inputmethod.EditorInfo;
18 import android.view.inputmethod.InputConnection;
19 import android.view.inputmethod.InputMethodManager;
20 import android.widget.RelativeLayout;
21 import android.widget.Button;
22 import android.widget.LinearLayout;
23 import android.widget.TextView;
24 import android.os.*;
25 import android.util.Log;
26 import android.util.SparseArray;
27 import android.graphics.*;
28 import android.graphics.drawable.Drawable;
29 import android.media.*;
30 import android.hardware.*;
31 import android.content.pm.ActivityInfo;
32 import java.io.*;
33 import android.content.res.AssetManager;
34 import android.content.res.Configuration;
35 
36 /**
37     SDL Activity
38 */
39 public class SDLActivity extends Activity {
40     private static final String TAG = "SDL";
41 
42     // Keep track of the paused state
43     public static boolean mIsPaused, mIsSurfaceReady, mHasFocus;
44     public static boolean mExitCalledFromJava;
45 
46     /** If shared libraries (e.g. SDL or the native application) could not be loaded. */
47     public static boolean mBrokenLibraries;
48 
49     // If we want to separate mouse and touch events.
50     //  This is only toggled in native code when a hint is set!
51     public static boolean mSeparateMouseAndTouch;
52 
53     // Main components
54     protected static SDLActivity mSingleton;
55     protected static SDLSurface mSurface;
56     protected static View mTextEdit;
57     protected static ViewGroup mLayout;
58     protected static SDLJoystickHandler mJoystickHandler;
59 
60     // This is what SDL runs in. It invokes SDL_main(), eventually
61     protected static Thread mSDLThread;
62 
63     // Audio
64     protected static AudioTrack mAudioTrack;
65     protected static AudioRecord mAudioRecord;
66 
67     /**
68      * This method is called by SDL before loading the native shared libraries.
69      * It can be overridden to provide names of shared libraries to be loaded.
70      * The default implementation returns the defaults. It never returns null.
71      * An array returned by a new implementation must at least contain "SDL2".
72      * Also keep in mind that the order the libraries are loaded may matter.
73      * @return names of shared libraries to be loaded (e.g. "SDL2", "main").
74      */
getLibraries()75     protected String[] getLibraries() {
76         return new String[] {
77             "SDL2",
78             // "SDL2_image",
79             // "SDL2_mixer",
80             // "SDL2_net",
81             // "SDL2_ttf",
82             "main"
83         };
84     }
85 
86     // Load the .so
loadLibraries()87     public void loadLibraries() {
88        for (String lib : getLibraries()) {
89           System.loadLibrary(lib);
90        }
91     }
92 
93     /**
94      * This method is called by SDL before starting the native application thread.
95      * It can be overridden to provide the arguments after the application name.
96      * The default implementation returns an empty array. It never returns null.
97      * @return arguments for the native application.
98      */
getArguments()99     protected String[] getArguments() {
100         return new String[0];
101     }
102 
initialize()103     public static void initialize() {
104         // The static nature of the singleton and Android quirkyness force us to initialize everything here
105         // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values
106         mSingleton = null;
107         mSurface = null;
108         mTextEdit = null;
109         mLayout = null;
110         mJoystickHandler = null;
111         mSDLThread = null;
112         mAudioTrack = null;
113         mAudioRecord = null;
114         mExitCalledFromJava = false;
115         mBrokenLibraries = false;
116         mIsPaused = false;
117         mIsSurfaceReady = false;
118         mHasFocus = true;
119     }
120 
121     // Setup
122     @Override
onCreate(Bundle savedInstanceState)123     protected void onCreate(Bundle savedInstanceState) {
124         Log.v(TAG, "Device: " + android.os.Build.DEVICE);
125         Log.v(TAG, "Model: " + android.os.Build.MODEL);
126         Log.v(TAG, "onCreate(): " + mSingleton);
127         super.onCreate(savedInstanceState);
128 
129         SDLActivity.initialize();
130         // So we can call stuff from static callbacks
131         mSingleton = this;
132 
133         // Load shared libraries
134         String errorMsgBrokenLib = "";
135         try {
136             loadLibraries();
137         } catch(UnsatisfiedLinkError e) {
138             System.err.println(e.getMessage());
139             mBrokenLibraries = true;
140             errorMsgBrokenLib = e.getMessage();
141         } catch(Exception e) {
142             System.err.println(e.getMessage());
143             mBrokenLibraries = true;
144             errorMsgBrokenLib = e.getMessage();
145         }
146 
147         if (mBrokenLibraries)
148         {
149             AlertDialog.Builder dlgAlert  = new AlertDialog.Builder(this);
150             dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall."
151                   + System.getProperty("line.separator")
152                   + System.getProperty("line.separator")
153                   + "Error: " + errorMsgBrokenLib);
154             dlgAlert.setTitle("SDL Error");
155             dlgAlert.setPositiveButton("Exit",
156                 new DialogInterface.OnClickListener() {
157                     @Override
158                     public void onClick(DialogInterface dialog,int id) {
159                         // if this button is clicked, close current activity
160                         SDLActivity.mSingleton.finish();
161                     }
162                 });
163            dlgAlert.setCancelable(false);
164            dlgAlert.create().show();
165 
166            return;
167         }
168 
169         // Set up the surface
170         mSurface = new SDLSurface(getApplication());
171 
172         if(Build.VERSION.SDK_INT >= 12) {
173             mJoystickHandler = new SDLJoystickHandler_API12();
174         }
175         else {
176             mJoystickHandler = new SDLJoystickHandler();
177         }
178 
179         mLayout = new RelativeLayout(this);
180         mLayout.addView(mSurface);
181 
182         setContentView(mLayout);
183 
184         // Get filename from "Open with" of another application
185         Intent intent = getIntent();
186 
187         if (intent != null && intent.getData() != null) {
188             String filename = intent.getData().getPath();
189             if (filename != null) {
190                 Log.v(TAG, "Got filename: " + filename);
191                 SDLActivity.onNativeDropFile(filename);
192             }
193         }
194 
195 		View decorView = getWindow().getDecorView();
196 		// Hide both the navigation bar and the status bar.
197 		// SYSTEM_UI_FLAG_FULLSCREEN is only available on Android 4.1 and higher, but as
198 		// a general rule, you should design your app to hide the status bar whenever you
199 		// hide the navigation bar.
200 		int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
201 					  | View.SYSTEM_UI_FLAG_FULLSCREEN;
202 		decorView.setSystemUiVisibility(uiOptions);
203     }
204 
205     // Events
206     @Override
onPause()207     protected void onPause() {
208         Log.v(TAG, "onPause()");
209         super.onPause();
210 
211         if (SDLActivity.mBrokenLibraries) {
212            return;
213         }
214 
215         SDLActivity.handlePause();
216     }
217 
218     @Override
onResume()219     protected void onResume() {
220         Log.v(TAG, "onResume()");
221         super.onResume();
222 
223         if (SDLActivity.mBrokenLibraries) {
224            return;
225         }
226 
227         SDLActivity.handleResume();
228     }
229 
230 
231     @Override
onWindowFocusChanged(boolean hasFocus)232     public void onWindowFocusChanged(boolean hasFocus) {
233         super.onWindowFocusChanged(hasFocus);
234         Log.v(TAG, "onWindowFocusChanged(): " + hasFocus);
235 
236         if (SDLActivity.mBrokenLibraries) {
237            return;
238         }
239 
240         SDLActivity.mHasFocus = hasFocus;
241         if (hasFocus) {
242             SDLActivity.handleResume();
243         }
244     }
245 
246     @Override
onLowMemory()247     public void onLowMemory() {
248         Log.v(TAG, "onLowMemory()");
249         super.onLowMemory();
250 
251         if (SDLActivity.mBrokenLibraries) {
252            return;
253         }
254 
255         SDLActivity.nativeLowMemory();
256     }
257 
258     @Override
onDestroy()259     protected void onDestroy() {
260         Log.v(TAG, "onDestroy()");
261 
262         if (SDLActivity.mBrokenLibraries) {
263            super.onDestroy();
264            // Reset everything in case the user re opens the app
265            SDLActivity.initialize();
266            return;
267         }
268 
269         // Send a quit message to the application
270         SDLActivity.mExitCalledFromJava = true;
271         SDLActivity.nativeQuit();
272 
273         // Now wait for the SDL thread to quit
274         if (SDLActivity.mSDLThread != null) {
275             try {
276                 SDLActivity.mSDLThread.join();
277             } catch(Exception e) {
278                 Log.v(TAG, "Problem stopping thread: " + e);
279             }
280             SDLActivity.mSDLThread = null;
281 
282             //Log.v(TAG, "Finished waiting for SDL thread");
283         }
284 
285         super.onDestroy();
286         // Reset everything in case the user re opens the app
287         SDLActivity.initialize();
288     }
289 
290     @Override
onBackPressed()291     public void onBackPressed() {
292         super.onBackPressed();
293     }
294 
295     @Override
dispatchKeyEvent(KeyEvent event)296     public boolean dispatchKeyEvent(KeyEvent event) {
297 
298         if (SDLActivity.mBrokenLibraries) {
299            return false;
300         }
301 
302         int keyCode = event.getKeyCode();
303         // Ignore certain special keys so they're handled by Android
304         if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
305             keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
306             keyCode == KeyEvent.KEYCODE_CAMERA ||
307             keyCode == 168 || /* API 11: KeyEvent.KEYCODE_ZOOM_IN */
308             keyCode == 169 /* API 11: KeyEvent.KEYCODE_ZOOM_OUT */
309             ) {
310             return false;
311         }
312         return super.dispatchKeyEvent(event);
313     }
314 
315     /** Called by onPause or surfaceDestroyed. Even if surfaceDestroyed
316      *  is the first to be called, mIsSurfaceReady should still be set
317      *  to 'true' during the call to onPause (in a usual scenario).
318      */
handlePause()319     public static void handlePause() {
320         if (!SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady) {
321             SDLActivity.mIsPaused = true;
322             SDLActivity.nativePause();
323             mSurface.handlePause();
324         }
325     }
326 
327     /** Called by onResume or surfaceCreated. An actual resume should be done only when the surface is ready.
328      * Note: Some Android variants may send multiple surfaceChanged events, so we don't need to resume
329      * every time we get one of those events, only if it comes after surfaceDestroyed
330      */
handleResume()331     public static void handleResume() {
332         if (SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady && SDLActivity.mHasFocus) {
333             SDLActivity.mIsPaused = false;
334             SDLActivity.nativeResume();
335             mSurface.handleResume();
336         }
337     }
338 
339     /* The native thread has finished */
handleNativeExit()340     public static void handleNativeExit() {
341         SDLActivity.mSDLThread = null;
342         mSingleton.finish();
343     }
344 
345 
346     // Messages from the SDLMain thread
347     static final int COMMAND_CHANGE_TITLE = 1;
348     static final int COMMAND_UNUSED = 2;
349     static final int COMMAND_TEXTEDIT_HIDE = 3;
350     static final int COMMAND_SET_KEEP_SCREEN_ON = 5;
351 
352     protected static final int COMMAND_USER = 0x8000;
353 
354     /**
355      * This method is called by SDL if SDL did not handle a message itself.
356      * This happens if a received message contains an unsupported command.
357      * Method can be overwritten to handle Messages in a different class.
358      * @param command the command of the message.
359      * @param param the parameter of the message. May be null.
360      * @return if the message was handled in overridden method.
361      */
onUnhandledMessage(int command, Object param)362     protected boolean onUnhandledMessage(int command, Object param) {
363         return false;
364     }
365 
366     /**
367      * A Handler class for Messages from native SDL applications.
368      * It uses current Activities as target (e.g. for the title).
369      * static to prevent implicit references to enclosing object.
370      */
371     protected static class SDLCommandHandler extends Handler {
372         @Override
handleMessage(Message msg)373         public void handleMessage(Message msg) {
374             Context context = getContext();
375             if (context == null) {
376                 Log.e(TAG, "error handling message, getContext() returned null");
377                 return;
378             }
379             switch (msg.arg1) {
380             case COMMAND_CHANGE_TITLE:
381                 if (context instanceof Activity) {
382                     ((Activity) context).setTitle((String)msg.obj);
383                 } else {
384                     Log.e(TAG, "error handling message, getContext() returned no Activity");
385                 }
386                 break;
387             case COMMAND_TEXTEDIT_HIDE:
388                 if (mTextEdit != null) {
389                     // Note: On some devices setting view to GONE creates a flicker in landscape.
390                     // Setting the View's sizes to 0 is similar to GONE but without the flicker.
391                     // The sizes will be set to useful values when the keyboard is shown again.
392                     mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0));
393 
394  //                   InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
395  //                   imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0);
396                 }
397                 break;
398             case COMMAND_SET_KEEP_SCREEN_ON:
399             {
400                 Window window = ((Activity) context).getWindow();
401                 if (window != null) {
402                     if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) {
403                         window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
404                     } else {
405                         window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
406                     }
407                 }
408                 break;
409             }
410             default:
411                 if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) {
412                     Log.e(TAG, "error handling message, command is " + msg.arg1);
413                 }
414             }
415         }
416     }
417 
418     // Handler for the messages
419     Handler commandHandler = new SDLCommandHandler();
420 
421     // Send a message from the SDLMain thread
sendCommand(int command, Object data)422     boolean sendCommand(int command, Object data) {
423         Message msg = commandHandler.obtainMessage();
424         msg.arg1 = command;
425         msg.obj = data;
426         return commandHandler.sendMessage(msg);
427     }
428 
429     // C functions we call
nativeInit(Object arguments)430     public static native int nativeInit(Object arguments);
nativeLowMemory()431     public static native void nativeLowMemory();
nativeQuit()432     public static native void nativeQuit();
nativePause()433     public static native void nativePause();
nativeResume()434     public static native void nativeResume();
onNativeDropFile(String filename)435     public static native void onNativeDropFile(String filename);
onNativeResize(int x, int y, int format, float rate)436     public static native void onNativeResize(int x, int y, int format, float rate);
onNativePadDown(int device_id, int keycode)437     public static native int onNativePadDown(int device_id, int keycode);
onNativePadUp(int device_id, int keycode)438     public static native int onNativePadUp(int device_id, int keycode);
onNativeJoy(int device_id, int axis, float value)439     public static native void onNativeJoy(int device_id, int axis,
440                                           float value);
onNativeHat(int device_id, int hat_id, int x, int y)441     public static native void onNativeHat(int device_id, int hat_id,
442                                           int x, int y);
onNativeKeyDown(int keycode)443     public static native void onNativeKeyDown(int keycode);
onNativeKeyUp(int keycode)444     public static native void onNativeKeyUp(int keycode);
onNativeKeyboardFocusLost()445     public static native void onNativeKeyboardFocusLost();
onNativeMouse(int button, int action, float x, float y)446     public static native void onNativeMouse(int button, int action, float x, float y);
onNativeTouch(int touchDevId, int pointerFingerId, int action, float x, float y, float p)447     public static native void onNativeTouch(int touchDevId, int pointerFingerId,
448                                             int action, float x,
449                                             float y, float p);
onNativeAccel(float x, float y, float z)450     public static native void onNativeAccel(float x, float y, float z);
onNativeSurfaceChanged()451     public static native void onNativeSurfaceChanged();
onNativeSurfaceDestroyed()452     public static native void onNativeSurfaceDestroyed();
nativeAddJoystick(int device_id, String name, int is_accelerometer, int nbuttons, int naxes, int nhats, int nballs)453     public static native int nativeAddJoystick(int device_id, String name,
454                                                int is_accelerometer, int nbuttons,
455                                                int naxes, int nhats, int nballs);
nativeRemoveJoystick(int device_id)456     public static native int nativeRemoveJoystick(int device_id);
nativeGetHint(String name)457     public static native String nativeGetHint(String name);
458 
459     /**
460      * This method is called by SDL using JNI.
461      */
setActivityTitle(String title)462     public static boolean setActivityTitle(String title) {
463         // Called from SDLMain() thread and can't directly affect the view
464         return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title);
465     }
466 
467     /**
468      * This method is called by SDL using JNI.
469      */
sendMessage(int command, int param)470     public static boolean sendMessage(int command, int param) {
471         return mSingleton.sendCommand(command, Integer.valueOf(param));
472     }
473 
474     /**
475      * This method is called by SDL using JNI.
476      */
getContext()477     public static Context getContext() {
478         return mSingleton;
479     }
480 
481     /**
482      * This method is called by SDL using JNI.
483      * @return result of getSystemService(name) but executed on UI thread.
484      */
getSystemServiceFromUiThread(final String name)485     public Object getSystemServiceFromUiThread(final String name) {
486         final Object lock = new Object();
487         final Object[] results = new Object[2]; // array for writable variables
488         synchronized (lock) {
489             runOnUiThread(new Runnable() {
490                 @Override
491                 public void run() {
492                     synchronized (lock) {
493                         results[0] = getSystemService(name);
494                         results[1] = Boolean.TRUE;
495                         lock.notify();
496                     }
497                 }
498             });
499             if (results[1] == null) {
500                 try {
501                     lock.wait();
502                 } catch (InterruptedException ex) {
503                     ex.printStackTrace();
504                 }
505             }
506         }
507         return results[0];
508     }
509 
510     static class ShowTextInputTask implements Runnable {
511         /*
512          * This is used to regulate the pan&scan method to have some offset from
513          * the bottom edge of the input region and the top edge of an input
514          * method (soft keyboard)
515          */
516         static final int HEIGHT_PADDING = 15;
517 
518         public int x, y, w, h;
519 
ShowTextInputTask(int x, int y, int w, int h)520         public ShowTextInputTask(int x, int y, int w, int h) {
521             this.x = x;
522             this.y = y;
523             this.w = w;
524             this.h = h;
525         }
526 
527         @Override
run()528         public void run() {
529             RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING);
530             params.leftMargin = x;
531             params.topMargin = y;
532 
533             if (mTextEdit == null) {
534                 mTextEdit = new DummyEdit(getContext());
535 
536                 mLayout.addView(mTextEdit, params);
537             } else {
538                 mTextEdit.setLayoutParams(params);
539             }
540 
541             mTextEdit.setVisibility(View.VISIBLE);
542             mTextEdit.requestFocus();
543 
544             InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
545             imm.showSoftInput(mTextEdit, 0);
546         }
547     }
548 
549     /**
550      * This method is called by SDL using JNI.
551      */
showTextInput(int x, int y, int w, int h)552     public static boolean showTextInput(int x, int y, int w, int h) {
553         // Transfer the task to the main thread as a Runnable
554 //        return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h));
555 		return true;
556     }
557 
558     /**
559      * This method is called by SDL using JNI.
560      */
getNativeSurface()561     public static Surface getNativeSurface() {
562         return SDLActivity.mSurface.getNativeSurface();
563     }
564 
565     // Audio
566 
567     /**
568      * This method is called by SDL using JNI.
569      */
audioOpen(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames)570     public static int audioOpen(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames) {
571         int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO;
572         int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT;
573         int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1);
574 
575         Log.v(TAG, "SDL audio: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer");
576 
577         // Let the user pick a larger buffer if they really want -- but ye
578         // gods they probably shouldn't, the minimums are horrifyingly high
579         // latency already
580         desiredFrames = Math.max(desiredFrames, (AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize);
581 
582         if (mAudioTrack == null) {
583             mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
584                     channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
585 
586             // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid
587             // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java
588             // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState()
589 
590             if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
591                 Log.e(TAG, "Failed during initialization of Audio Track");
592                 mAudioTrack = null;
593                 return -1;
594             }
595 
596             mAudioTrack.play();
597         }
598 
599         Log.v(TAG, "SDL audio: got " + ((mAudioTrack.getChannelCount() >= 2) ? "stereo" : "mono") + " " + ((mAudioTrack.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) ? "16-bit" : "8-bit") + " " + (mAudioTrack.getSampleRate() / 1000f) + "kHz, " + desiredFrames + " frames buffer");
600 
601         return 0;
602     }
603 
604     /**
605      * This method is called by SDL using JNI.
606      */
audioWriteShortBuffer(short[] buffer)607     public static void audioWriteShortBuffer(short[] buffer) {
608         for (int i = 0; i < buffer.length; ) {
609             int result = mAudioTrack.write(buffer, i, buffer.length - i);
610             if (result > 0) {
611                 i += result;
612             } else if (result == 0) {
613                 try {
614                     Thread.sleep(1);
615                 } catch(InterruptedException e) {
616                     // Nom nom
617                 }
618             } else {
619                 Log.w(TAG, "SDL audio: error return from write(short)");
620                 return;
621             }
622         }
623     }
624 
625     /**
626      * This method is called by SDL using JNI.
627      */
audioWriteByteBuffer(byte[] buffer)628     public static void audioWriteByteBuffer(byte[] buffer) {
629         for (int i = 0; i < buffer.length; ) {
630             int result = mAudioTrack.write(buffer, i, buffer.length - i);
631             if (result > 0) {
632                 i += result;
633             } else if (result == 0) {
634                 try {
635                     Thread.sleep(1);
636                 } catch(InterruptedException e) {
637                     // Nom nom
638                 }
639             } else {
640                 Log.w(TAG, "SDL audio: error return from write(byte)");
641                 return;
642             }
643         }
644     }
645 
646     /**
647      * This method is called by SDL using JNI.
648      */
captureOpen(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames)649     public static int captureOpen(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames) {
650         int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO;
651         int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT;
652         int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1);
653 
654         Log.v(TAG, "SDL capture: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer");
655 
656         // Let the user pick a larger buffer if they really want -- but ye
657         // gods they probably shouldn't, the minimums are horrifyingly high
658         // latency already
659         desiredFrames = Math.max(desiredFrames, (AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize);
660 
661         if (mAudioRecord == null) {
662             mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate,
663                     channelConfig, audioFormat, desiredFrames * frameSize);
664 
665             // see notes about AudioTrack state in audioOpen(), above. Probably also applies here.
666             if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
667                 Log.e(TAG, "Failed during initialization of AudioRecord");
668                 mAudioRecord.release();
669                 mAudioRecord = null;
670                 return -1;
671             }
672 
673             mAudioRecord.startRecording();
674         }
675 
676         Log.v(TAG, "SDL capture: got " + ((mAudioRecord.getChannelCount() >= 2) ? "stereo" : "mono") + " " + ((mAudioRecord.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) ? "16-bit" : "8-bit") + " " + (mAudioRecord.getSampleRate() / 1000f) + "kHz, " + desiredFrames + " frames buffer");
677 
678         return 0;
679     }
680 
681     /** This method is called by SDL using JNI. */
captureReadShortBuffer(short[] buffer, boolean blocking)682     public static int captureReadShortBuffer(short[] buffer, boolean blocking) {
683         // !!! FIXME: this is available in API Level 23. Until then, we always block.  :(
684         //return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
685         return mAudioRecord.read(buffer, 0, buffer.length);
686     }
687 
688     /** This method is called by SDL using JNI. */
captureReadByteBuffer(byte[] buffer, boolean blocking)689     public static int captureReadByteBuffer(byte[] buffer, boolean blocking) {
690         // !!! FIXME: this is available in API Level 23. Until then, we always block.  :(
691         //return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
692         return mAudioRecord.read(buffer, 0, buffer.length);
693     }
694 
695 
696     /** This method is called by SDL using JNI. */
audioClose()697     public static void audioClose() {
698         if (mAudioTrack != null) {
699             mAudioTrack.stop();
700             mAudioTrack.release();
701             mAudioTrack = null;
702         }
703     }
704 
705     /** This method is called by SDL using JNI. */
captureClose()706     public static void captureClose() {
707         if (mAudioRecord != null) {
708             mAudioRecord.stop();
709             mAudioRecord.release();
710             mAudioRecord = null;
711         }
712     }
713 
714 
715     // Input
716 
717     /**
718      * This method is called by SDL using JNI.
719      * @return an array which may be empty but is never null.
720      */
inputGetInputDeviceIds(int sources)721     public static int[] inputGetInputDeviceIds(int sources) {
722         int[] ids = InputDevice.getDeviceIds();
723         int[] filtered = new int[ids.length];
724         int used = 0;
725         for (int i = 0; i < ids.length; ++i) {
726             InputDevice device = InputDevice.getDevice(ids[i]);
727             if ((device != null) && ((device.getSources() & sources) != 0)) {
728                 filtered[used++] = device.getId();
729             }
730         }
731         return Arrays.copyOf(filtered, used);
732     }
733 
734     // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
handleJoystickMotionEvent(MotionEvent event)735     public static boolean handleJoystickMotionEvent(MotionEvent event) {
736         return mJoystickHandler.handleMotionEvent(event);
737     }
738 
739     /**
740      * This method is called by SDL using JNI.
741      */
pollInputDevices()742     public static void pollInputDevices() {
743         if (SDLActivity.mSDLThread != null) {
744             mJoystickHandler.pollInputDevices();
745         }
746     }
747 
748     // Check if a given device is considered a possible SDL joystick
isDeviceSDLJoystick(int deviceId)749     public static boolean isDeviceSDLJoystick(int deviceId) {
750         InputDevice device = InputDevice.getDevice(deviceId);
751         // We cannot use InputDevice.isVirtual before API 16, so let's accept
752         // only nonnegative device ids (VIRTUAL_KEYBOARD equals -1)
753         if ((device == null) || (deviceId < 0)) {
754             return false;
755         }
756         int sources = device.getSources();
757         return (((sources & InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_CLASS_JOYSTICK) ||
758                 ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) ||
759                 ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
760         );
761     }
762 
763     // APK expansion files support
764 
765     /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */
766     private Object expansionFile;
767 
768     /** com.android.vending.expansion.zipfile.ZipResourceFile's getInputStream() or null. */
769     private Method expansionFileMethod;
770 
771     /**
772      * This method was called by SDL using JNI.
773      * @deprecated because of an incorrect name
774      */
775     @Deprecated
openAPKExtensionInputStream(String fileName)776     public InputStream openAPKExtensionInputStream(String fileName) throws IOException {
777         return openAPKExpansionInputStream(fileName);
778     }
779 
780     /**
781      * This method is called by SDL using JNI.
782      * @return an InputStream on success or null if no expansion file was used.
783      * @throws IOException on errors. Message is set for the SDL error message.
784      */
openAPKExpansionInputStream(String fileName)785     public InputStream openAPKExpansionInputStream(String fileName) throws IOException {
786         // Get a ZipResourceFile representing a merger of both the main and patch files
787         if (expansionFile == null) {
788             String mainHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION");
789             if (mainHint == null) {
790                 return null; // no expansion use if no main version was set
791             }
792             String patchHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION");
793             if (patchHint == null) {
794                 return null; // no expansion use if no patch version was set
795             }
796 
797             Integer mainVersion;
798             Integer patchVersion;
799             try {
800                 mainVersion = Integer.valueOf(mainHint);
801                 patchVersion = Integer.valueOf(patchHint);
802             } catch (NumberFormatException ex) {
803                 ex.printStackTrace();
804                 throw new IOException("No valid file versions set for APK expansion files", ex);
805             }
806 
807             try {
808                 // To avoid direct dependency on Google APK expansion library that is
809                 // not a part of Android SDK we access it using reflection
810                 expansionFile = Class.forName("com.android.vending.expansion.zipfile.APKExpansionSupport")
811                     .getMethod("getAPKExpansionZipFile", Context.class, int.class, int.class)
812                     .invoke(null, this, mainVersion, patchVersion);
813 
814                 expansionFileMethod = expansionFile.getClass()
815                     .getMethod("getInputStream", String.class);
816             } catch (Exception ex) {
817                 ex.printStackTrace();
818                 expansionFile = null;
819                 expansionFileMethod = null;
820                 throw new IOException("Could not access APK expansion support library", ex);
821             }
822         }
823 
824         // Get an input stream for a known file inside the expansion file ZIPs
825         InputStream fileStream;
826         try {
827             fileStream = (InputStream)expansionFileMethod.invoke(expansionFile, fileName);
828         } catch (Exception ex) {
829             // calling "getInputStream" failed
830             ex.printStackTrace();
831             throw new IOException("Could not open stream from APK expansion file", ex);
832         }
833 
834         if (fileStream == null) {
835             // calling "getInputStream" was successful but null was returned
836             throw new IOException("Could not find path in APK expansion file");
837         }
838 
839         return fileStream;
840     }
841 
842     // Messagebox
843 
844     /** Result of current messagebox. Also used for blocking the calling thread. */
845     protected final int[] messageboxSelection = new int[1];
846 
847     /** Id of current dialog. */
848     protected int dialogs = 0;
849 
850     /**
851      * This method is called by SDL using JNI.
852      * Shows the messagebox from UI thread and block calling thread.
853      * buttonFlags, buttonIds and buttonTexts must have same length.
854      * @param buttonFlags array containing flags for every button.
855      * @param buttonIds array containing id for every button.
856      * @param buttonTexts array containing text for every button.
857      * @param colors null for default or array of length 5 containing colors.
858      * @return button id or -1.
859      */
messageboxShowMessageBox( final int flags, final String title, final String message, final int[] buttonFlags, final int[] buttonIds, final String[] buttonTexts, final int[] colors)860     public int messageboxShowMessageBox(
861             final int flags,
862             final String title,
863             final String message,
864             final int[] buttonFlags,
865             final int[] buttonIds,
866             final String[] buttonTexts,
867             final int[] colors) {
868 
869         messageboxSelection[0] = -1;
870 
871         // sanity checks
872 
873         if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) {
874             return -1; // implementation broken
875         }
876 
877         // collect arguments for Dialog
878 
879         final Bundle args = new Bundle();
880         args.putInt("flags", flags);
881         args.putString("title", title);
882         args.putString("message", message);
883         args.putIntArray("buttonFlags", buttonFlags);
884         args.putIntArray("buttonIds", buttonIds);
885         args.putStringArray("buttonTexts", buttonTexts);
886         args.putIntArray("colors", colors);
887 
888         // trigger Dialog creation on UI thread
889 
890         runOnUiThread(new Runnable() {
891             @Override
892             public void run() {
893                 showDialog(dialogs++, args);
894             }
895         });
896 
897         // block the calling thread
898 
899         synchronized (messageboxSelection) {
900             try {
901                 messageboxSelection.wait();
902             } catch (InterruptedException ex) {
903                 ex.printStackTrace();
904                 return -1;
905             }
906         }
907 
908         // return selected value
909 
910         return messageboxSelection[0];
911     }
912 
913     @Override
onCreateDialog(int ignore, Bundle args)914     protected Dialog onCreateDialog(int ignore, Bundle args) {
915 
916         // TODO set values from "flags" to messagebox dialog
917 
918         // get colors
919 
920         int[] colors = args.getIntArray("colors");
921         int backgroundColor;
922         int textColor;
923         int buttonBorderColor;
924         int buttonBackgroundColor;
925         int buttonSelectedColor;
926         if (colors != null) {
927             int i = -1;
928             backgroundColor = colors[++i];
929             textColor = colors[++i];
930             buttonBorderColor = colors[++i];
931             buttonBackgroundColor = colors[++i];
932             buttonSelectedColor = colors[++i];
933         } else {
934             backgroundColor = Color.TRANSPARENT;
935             textColor = Color.TRANSPARENT;
936             buttonBorderColor = Color.TRANSPARENT;
937             buttonBackgroundColor = Color.TRANSPARENT;
938             buttonSelectedColor = Color.TRANSPARENT;
939         }
940 
941         // create dialog with title and a listener to wake up calling thread
942 
943         final Dialog dialog = new Dialog(this);
944         dialog.setTitle(args.getString("title"));
945         dialog.setCancelable(false);
946         dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
947             @Override
948             public void onDismiss(DialogInterface unused) {
949                 synchronized (messageboxSelection) {
950                     messageboxSelection.notify();
951                 }
952             }
953         });
954 
955         // create text
956 
957         TextView message = new TextView(this);
958         message.setGravity(Gravity.CENTER);
959         message.setText(args.getString("message"));
960         if (textColor != Color.TRANSPARENT) {
961             message.setTextColor(textColor);
962         }
963 
964         // create buttons
965 
966         int[] buttonFlags = args.getIntArray("buttonFlags");
967         int[] buttonIds = args.getIntArray("buttonIds");
968         String[] buttonTexts = args.getStringArray("buttonTexts");
969 
970         final SparseArray<Button> mapping = new SparseArray<Button>();
971 
972         LinearLayout buttons = new LinearLayout(this);
973         buttons.setOrientation(LinearLayout.HORIZONTAL);
974         buttons.setGravity(Gravity.CENTER);
975         for (int i = 0; i < buttonTexts.length; ++i) {
976             Button button = new Button(this);
977             final int id = buttonIds[i];
978             button.setOnClickListener(new View.OnClickListener() {
979                 @Override
980                 public void onClick(View v) {
981                     messageboxSelection[0] = id;
982                     dialog.dismiss();
983                 }
984             });
985             if (buttonFlags[i] != 0) {
986                 // see SDL_messagebox.h
987                 if ((buttonFlags[i] & 0x00000001) != 0) {
988                     mapping.put(KeyEvent.KEYCODE_ENTER, button);
989                 }
990                 if ((buttonFlags[i] & 0x00000002) != 0) {
991                     mapping.put(111, button); /* API 11: KeyEvent.KEYCODE_ESCAPE */
992                 }
993             }
994             button.setText(buttonTexts[i]);
995             if (textColor != Color.TRANSPARENT) {
996                 button.setTextColor(textColor);
997             }
998             if (buttonBorderColor != Color.TRANSPARENT) {
999                 // TODO set color for border of messagebox button
1000             }
1001             if (buttonBackgroundColor != Color.TRANSPARENT) {
1002                 Drawable drawable = button.getBackground();
1003                 if (drawable == null) {
1004                     // setting the color this way removes the style
1005                     button.setBackgroundColor(buttonBackgroundColor);
1006                 } else {
1007                     // setting the color this way keeps the style (gradient, padding, etc.)
1008                     drawable.setColorFilter(buttonBackgroundColor, PorterDuff.Mode.MULTIPLY);
1009                 }
1010             }
1011             if (buttonSelectedColor != Color.TRANSPARENT) {
1012                 // TODO set color for selected messagebox button
1013             }
1014             buttons.addView(button);
1015         }
1016 
1017         // create content
1018 
1019         LinearLayout content = new LinearLayout(this);
1020         content.setOrientation(LinearLayout.VERTICAL);
1021         content.addView(message);
1022         content.addView(buttons);
1023         if (backgroundColor != Color.TRANSPARENT) {
1024             content.setBackgroundColor(backgroundColor);
1025         }
1026 
1027         // add content to dialog and return
1028 
1029         dialog.setContentView(content);
1030         dialog.setOnKeyListener(new Dialog.OnKeyListener() {
1031             @Override
1032             public boolean onKey(DialogInterface d, int keyCode, KeyEvent event) {
1033                 Button button = mapping.get(keyCode);
1034                 if (button != null) {
1035                     if (event.getAction() == KeyEvent.ACTION_UP) {
1036                         button.performClick();
1037                     }
1038                     return true; // also for ignored actions
1039                 }
1040                 return false;
1041             }
1042         });
1043 
1044         return dialog;
1045     }
1046 }
1047 
1048 /**
1049     Simple nativeInit() runnable
1050 */
1051 class SDLMain implements Runnable {
1052     @Override
run()1053     public void run() {
1054         // Runs SDL_main()
1055         SDLActivity.nativeInit(SDLActivity.mSingleton.getArguments());
1056 
1057         //Log.v("SDL", "SDL thread terminated");
1058     }
1059 }
1060 
1061 
1062 /**
1063     SDLSurface. This is what we draw on, so we need to know when it's created
1064     in order to do anything useful.
1065 
1066     Because of this, that's where we set up the SDL thread
1067 */
1068 class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
1069     View.OnKeyListener, View.OnTouchListener, SensorEventListener  {
1070 
1071     // Sensors
1072     protected static SensorManager mSensorManager;
1073     protected static Display mDisplay;
1074 
1075     // Keep track of the surface size to normalize touch events
1076     protected static float mWidth, mHeight;
1077 	static float[] px, py;
1078 
1079     // Startup
SDLSurface(Context context)1080     public SDLSurface(Context context) {
1081         super(context);
1082         getHolder().addCallback(this);
1083 
1084         setFocusable(true);
1085         setFocusableInTouchMode(true);
1086         requestFocus();
1087         setOnKeyListener(this);
1088         setOnTouchListener(this);
1089 
1090         mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
1091         mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
1092 
1093         if(Build.VERSION.SDK_INT >= 12) {
1094             setOnGenericMotionListener(new SDLGenericMotionListener_API12());
1095         }
1096         // Some arbitrary defaults to avoid a potential division by zero
1097         mWidth = 1.0f;
1098         mHeight = 1.0f;
1099 		px = new float[10];
1100 		py = new float[10];
1101     }
1102 
handlePause()1103     public void handlePause() {
1104         enableSensor(Sensor.TYPE_ACCELEROMETER, false);
1105     }
1106 
handleResume()1107     public void handleResume() {
1108         setFocusable(true);
1109         setFocusableInTouchMode(true);
1110         requestFocus();
1111         setOnKeyListener(this);
1112         setOnTouchListener(this);
1113         enableSensor(Sensor.TYPE_ACCELEROMETER, true);
1114     }
1115 
getNativeSurface()1116     public Surface getNativeSurface() {
1117 		Log.v("SDL", "getNativeSurface()");
1118         return getHolder().getSurface();
1119     }
1120 
1121     // Called when we have a valid drawing surface
1122     @Override
surfaceCreated(SurfaceHolder holder)1123     public void surfaceCreated(SurfaceHolder holder) {
1124         Log.v("SDL", "surfaceCreated()");
1125         holder.setType(SurfaceHolder.SURFACE_TYPE_GPU);
1126     }
1127 
1128     // Called when we lose the surface
1129     @Override
surfaceDestroyed(SurfaceHolder holder)1130     public void surfaceDestroyed(SurfaceHolder holder) {
1131         Log.v("SDL", "surfaceDestroyed()");
1132         // Call this *before* setting mIsSurfaceReady to 'false'
1133         SDLActivity.handlePause();
1134         SDLActivity.mIsSurfaceReady = false;
1135         SDLActivity.onNativeSurfaceDestroyed();
1136     }
1137 
1138     // Called when the surface is resized
1139     @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)1140     public void surfaceChanged(SurfaceHolder holder,
1141                                int format, int width, int height) {
1142         Log.v("SDL", "surfaceChanged()");
1143 
1144         int sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565 by default
1145         switch (format) {
1146         case PixelFormat.A_8:
1147             Log.v("SDL", "pixel format A_8");
1148             break;
1149         case PixelFormat.LA_88:
1150             Log.v("SDL", "pixel format LA_88");
1151             break;
1152         case PixelFormat.L_8:
1153             Log.v("SDL", "pixel format L_8");
1154             break;
1155         case PixelFormat.RGBA_4444:
1156             Log.v("SDL", "pixel format RGBA_4444");
1157             sdlFormat = 0x15421002; // SDL_PIXELFORMAT_RGBA4444
1158             break;
1159         case PixelFormat.RGBA_5551:
1160             Log.v("SDL", "pixel format RGBA_5551");
1161             sdlFormat = 0x15441002; // SDL_PIXELFORMAT_RGBA5551
1162             break;
1163         case PixelFormat.RGBA_8888:
1164             Log.v("SDL", "pixel format RGBA_8888");
1165             sdlFormat = 0x16462004; // SDL_PIXELFORMAT_RGBA8888
1166             break;
1167         case PixelFormat.RGBX_8888:
1168             Log.v("SDL", "pixel format RGBX_8888");
1169             sdlFormat = 0x16261804; // SDL_PIXELFORMAT_RGBX8888
1170             break;
1171         case PixelFormat.RGB_332:
1172             Log.v("SDL", "pixel format RGB_332");
1173             sdlFormat = 0x14110801; // SDL_PIXELFORMAT_RGB332
1174             break;
1175         case PixelFormat.RGB_565:
1176             Log.v("SDL", "pixel format RGB_565");
1177             sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565
1178             break;
1179         case PixelFormat.RGB_888:
1180             Log.v("SDL", "pixel format RGB_888");
1181             // Not sure this is right, maybe SDL_PIXELFORMAT_RGB24 instead?
1182             sdlFormat = 0x16161804; // SDL_PIXELFORMAT_RGB888
1183             break;
1184         default:
1185             Log.v("SDL", "pixel format unknown " + format);
1186             break;
1187         }
1188 
1189         mWidth = width;
1190         mHeight = height;
1191         SDLActivity.onNativeResize(width, height, sdlFormat, mDisplay.getRefreshRate());
1192         //Log.v("SDL", "Window size: " + width + "x" + height);
1193 
1194         boolean skip = false;
1195         int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
1196 
1197         if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
1198         {
1199             // Accept any
1200         }
1201         else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
1202         {
1203             if (mWidth > mHeight) {
1204                skip = true;
1205             }
1206         } else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
1207             if (mWidth < mHeight) {
1208                skip = true;
1209             }
1210         }
1211 
1212         // Special Patch for Square Resolution: Black Berry Passport
1213         if (skip) {
1214            double min = Math.min(mWidth, mHeight);
1215            double max = Math.max(mWidth, mHeight);
1216 
1217            if (max / min < 1.20) {
1218               Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
1219               skip = false;
1220            }
1221         }
1222 
1223         if (skip) {
1224            Log.v("SDL", "Skip .. Surface is not ready.");
1225            return;
1226         }
1227 
1228 
1229         // Set mIsSurfaceReady to 'true' *before* making a call to handleResume
1230         SDLActivity.mIsSurfaceReady = true;
1231         SDLActivity.onNativeSurfaceChanged();
1232 
1233 
1234         if (SDLActivity.mSDLThread == null) {
1235             // This is the entry point to the C app.
1236             // Start up the C app thread and enable sensor input for the first time
1237 
1238             final Thread sdlThread = new Thread(new SDLMain(), "SDLThread");
1239             enableSensor(Sensor.TYPE_ACCELEROMETER, true);
1240 
1241             // Set up a listener thread to catch when the native thread ends
1242             SDLActivity.mSDLThread = new Thread(new Runnable(){
1243                 @Override
1244                 public void run(){
1245                     try {
1246 						sdlThread.start();
1247                         sdlThread.join();
1248                     }
1249                     catch(Exception e){}
1250                     finally{
1251                         // Native thread has finished
1252                         if (! SDLActivity.mExitCalledFromJava) {
1253                             SDLActivity.handleNativeExit();
1254                         }
1255                     }
1256                 }
1257             }, "SDLThreadListener");
1258             SDLActivity.mSDLThread.start();
1259         }
1260 
1261         if (SDLActivity.mHasFocus) {
1262             SDLActivity.handleResume();
1263         }
1264     }
1265 
1266     // Key events
1267     @Override
onKey(View v, int keyCode, KeyEvent event)1268     public boolean onKey(View  v, int keyCode, KeyEvent event) {
1269         // Dispatch the different events depending on where they come from
1270         // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD
1271         // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD
1272         //
1273         // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and
1274         // SOURCE_JOYSTICK, while its key events arrive from the keyboard source
1275         // So, retrieve the device itself and check all of its sources
1276         if (SDLActivity.isDeviceSDLJoystick(event.getDeviceId())) {
1277             // Note that we process events with specific key codes here
1278             if (event.getAction() == KeyEvent.ACTION_DOWN) {
1279                 if (SDLActivity.onNativePadDown(event.getDeviceId(), keyCode) == 0) {
1280                     return true;
1281                 }
1282             } else if (event.getAction() == KeyEvent.ACTION_UP) {
1283                 if (SDLActivity.onNativePadUp(event.getDeviceId(), keyCode) == 0) {
1284                     return true;
1285                 }
1286             }
1287         }
1288 
1289         if ((event.getSource() & InputDevice.SOURCE_KEYBOARD) != 0) {
1290             if (event.getAction() == KeyEvent.ACTION_DOWN) {
1291                 //Log.v("SDL", "key down: " + keyCode);
1292                 SDLActivity.onNativeKeyDown(keyCode);
1293                 return true;
1294             }
1295             else if (event.getAction() == KeyEvent.ACTION_UP) {
1296                 //Log.v("SDL", "key up: " + keyCode);
1297                 SDLActivity.onNativeKeyUp(keyCode);
1298                 return true;
1299             }
1300         }
1301 
1302         if ((event.getSource() & InputDevice.SOURCE_MOUSE) != 0) {
1303             // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses
1304             // they are ignored here because sending them as mouse input to SDL is messy
1305             if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) {
1306                 switch (event.getAction()) {
1307                 case KeyEvent.ACTION_DOWN:
1308                 case KeyEvent.ACTION_UP:
1309                     // mark the event as handled or it will be handled by system
1310                     // handling KEYCODE_BACK by system will call onBackPressed()
1311                     return true;
1312                 }
1313             }
1314         }
1315 
1316         return false;
1317     }
1318 
1319     // Touch events
1320     @Override
onTouch(View v, MotionEvent event)1321     public boolean onTouch(View v, MotionEvent event) {
1322         /* Ref: http://developer.android.com/training/gestures/multi.html */
1323         final int touchDevId = event.getDeviceId();
1324         final int pointerCount = event.getPointerCount();
1325         int action = event.getActionMasked();
1326         int pointerFingerId;
1327         int mouseButton;
1328         int i = -1;
1329         float x,y,p;
1330 
1331         // !!! FIXME: dump this SDK check after 2.0.4 ships and require API14.
1332         if (event.getSource() == InputDevice.SOURCE_MOUSE && SDLActivity.mSeparateMouseAndTouch) {
1333             if (Build.VERSION.SDK_INT < 14) {
1334                 mouseButton = 1; // all mouse buttons are the left button
1335             } else {
1336                 try {
1337                     mouseButton = (Integer) event.getClass().getMethod("getButtonState").invoke(event);
1338                 } catch(Exception e) {
1339                     mouseButton = 1;    // oh well.
1340                 }
1341             }
1342             SDLActivity.onNativeMouse(mouseButton, action, event.getX(0), event.getY(0));
1343         } else {
1344             switch(action) {
1345                 case MotionEvent.ACTION_MOVE:
1346                     for (i = 0; i < pointerCount; i++) {
1347                         pointerFingerId = event.getPointerId(i);
1348                         x = event.getX(i) / mWidth;
1349                         y = event.getY(i) / mHeight;
1350                         p = event.getPressure(i);
1351                         if (p > 1.0f) {
1352                             // may be larger than 1.0f on some devices
1353                             // see the documentation of getPressure(i)
1354                             p = 1.0f;
1355                         }
1356 //                        SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
1357                         SDLActivity.onNativeMouse(1, action, event.getX(i) - px[i], event.getY(i) - py[i]);
1358 						px[i] = event.getX(i);
1359 						py[i] = event.getY(i);
1360                     }
1361                     break;
1362 
1363                 case MotionEvent.ACTION_UP:
1364                 case MotionEvent.ACTION_DOWN:
1365 					//SDLActivity.onNativeKeyDown(KeyEvent.KEYCODE_ENTER);
1366                     // Primary pointer up/down, the index is always zero
1367                     i = 0;
1368                 case MotionEvent.ACTION_POINTER_UP:
1369                 case MotionEvent.ACTION_POINTER_DOWN:
1370                     // Non primary pointer up/down
1371                     if (i == -1) {
1372                         i = event.getActionIndex();
1373                     }
1374 
1375                     pointerFingerId = event.getPointerId(i);
1376                     x = event.getX(i) / mWidth;
1377                     y = event.getY(i) / mHeight;
1378                     p = event.getPressure(i);
1379                     if (p > 1.0f) {
1380                         // may be larger than 1.0f on some devices
1381                         // see the documentation of getPressure(i)
1382                         p = 1.0f;
1383                     }
1384                     SDLActivity.onNativeMouse(1, action, 0, 0);
1385                     //SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
1386                     break;
1387 
1388                 case MotionEvent.ACTION_CANCEL:
1389                     for (i = 0; i < pointerCount; i++) {
1390                         pointerFingerId = event.getPointerId(i);
1391                         x = event.getX(i) / mWidth;
1392                         y = event.getY(i) / mHeight;
1393                         p = event.getPressure(i);
1394                         if (p > 1.0f) {
1395                             // may be larger than 1.0f on some devices
1396                             // see the documentation of getPressure(i)
1397                             p = 1.0f;
1398                         }
1399                         //SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
1400                     }
1401                     break;
1402 
1403                 default:
1404                     break;
1405             }
1406         }
1407 
1408         return true;
1409    }
1410 
1411     // Sensor events
enableSensor(int sensortype, boolean enabled)1412     public void enableSensor(int sensortype, boolean enabled) {
1413         // TODO: This uses getDefaultSensor - what if we have >1 accels?
1414         if (enabled) {
1415             mSensorManager.registerListener(this,
1416                             mSensorManager.getDefaultSensor(sensortype),
1417                             SensorManager.SENSOR_DELAY_GAME, null);
1418         } else {
1419             mSensorManager.unregisterListener(this,
1420                             mSensorManager.getDefaultSensor(sensortype));
1421         }
1422     }
1423 
1424     @Override
onAccuracyChanged(Sensor sensor, int accuracy)1425     public void onAccuracyChanged(Sensor sensor, int accuracy) {
1426         // TODO
1427     }
1428 
1429     @Override
onSensorChanged(SensorEvent event)1430     public void onSensorChanged(SensorEvent event) {
1431         if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
1432             float x, y;
1433             switch (mDisplay.getRotation()) {
1434                 case Surface.ROTATION_90:
1435                     x = -event.values[1];
1436                     y = event.values[0];
1437                     break;
1438                 case Surface.ROTATION_270:
1439                     x = event.values[1];
1440                     y = -event.values[0];
1441                     break;
1442                 case Surface.ROTATION_180:
1443                     x = -event.values[1];
1444                     y = -event.values[0];
1445                     break;
1446                 default:
1447                     x = event.values[0];
1448                     y = event.values[1];
1449                     break;
1450             }
1451             SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
1452                                       y / SensorManager.GRAVITY_EARTH,
1453                                       event.values[2] / SensorManager.GRAVITY_EARTH);
1454         }
1455     }
1456 }
1457 
1458 /* This is a fake invisible editor view that receives the input and defines the
1459  * pan&scan region
1460  */
1461 class DummyEdit extends View implements View.OnKeyListener {
1462     InputConnection ic;
1463 
DummyEdit(Context context)1464     public DummyEdit(Context context) {
1465         super(context);
1466         setFocusableInTouchMode(true);
1467         setFocusable(true);
1468         setOnKeyListener(this);
1469     }
1470 
1471     @Override
onCheckIsTextEditor()1472     public boolean onCheckIsTextEditor() {
1473         return true;
1474     }
1475 
1476     @Override
onKey(View v, int keyCode, KeyEvent event)1477     public boolean onKey(View v, int keyCode, KeyEvent event) {
1478 
1479         // This handles the hardware keyboard input
1480         if (event.isPrintingKey() || keyCode == KeyEvent.KEYCODE_SPACE) {
1481             if (event.getAction() == KeyEvent.ACTION_DOWN) {
1482                 ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1);
1483             }
1484             return true;
1485         }
1486 
1487         if (event.getAction() == KeyEvent.ACTION_DOWN) {
1488             SDLActivity.onNativeKeyDown(keyCode);
1489             return true;
1490         } else if (event.getAction() == KeyEvent.ACTION_UP) {
1491             SDLActivity.onNativeKeyUp(keyCode);
1492             return true;
1493 		}
1494         return false;
1495     }
1496 
1497     //
1498     @Override
onKeyPreIme(int keyCode, KeyEvent event)1499     public boolean onKeyPreIme (int keyCode, KeyEvent event) {
1500         // As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
1501         // FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
1502         // FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
1503         // FIXME: A more effective solution would be to change our Layout from AbsoluteLayout to Relative or Linear
1504         // FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
1505         // FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
1506         if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
1507             if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) {
1508                 SDLActivity.onNativeKeyboardFocusLost();
1509             }
1510         }
1511         return super.onKeyPreIme(keyCode, event);
1512     }
1513 
1514     @Override
onCreateInputConnection(EditorInfo outAttrs)1515     public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
1516         ic = new SDLInputConnection(this, true);
1517 
1518         outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
1519         outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
1520                 | 33554432 /* API 11: EditorInfo.IME_FLAG_NO_FULLSCREEN */;
1521 
1522         return ic;
1523     }
1524 }
1525 
1526 class SDLInputConnection extends BaseInputConnection {
1527 
SDLInputConnection(View targetView, boolean fullEditor)1528     public SDLInputConnection(View targetView, boolean fullEditor) {
1529         super(targetView, fullEditor);
1530 
1531     }
1532 
1533     @Override
sendKeyEvent(KeyEvent event)1534     public boolean sendKeyEvent(KeyEvent event) {
1535 
1536         /*
1537          * This handles the keycodes from soft keyboard (and IME-translated
1538          * input from hardkeyboard)
1539          */
1540         int keyCode = event.getKeyCode();
1541         if (event.getAction() == KeyEvent.ACTION_DOWN) {
1542             if (event.isPrintingKey() || keyCode == KeyEvent.KEYCODE_SPACE) {
1543                 commitText(String.valueOf((char) event.getUnicodeChar()), 1);
1544             }
1545             SDLActivity.onNativeKeyDown(keyCode);
1546             return true;
1547         } else if (event.getAction() == KeyEvent.ACTION_UP) {
1548 
1549             SDLActivity.onNativeKeyUp(keyCode);
1550             return true;
1551         }
1552         return super.sendKeyEvent(event);
1553     }
1554 
1555     @Override
commitText(CharSequence text, int newCursorPosition)1556     public boolean commitText(CharSequence text, int newCursorPosition) {
1557 
1558         nativeCommitText(text.toString(), newCursorPosition);
1559 
1560         return super.commitText(text, newCursorPosition);
1561     }
1562 
1563     @Override
setComposingText(CharSequence text, int newCursorPosition)1564     public boolean setComposingText(CharSequence text, int newCursorPosition) {
1565 
1566         nativeSetComposingText(text.toString(), newCursorPosition);
1567 
1568         return super.setComposingText(text, newCursorPosition);
1569     }
1570 
nativeCommitText(String text, int newCursorPosition)1571     public native void nativeCommitText(String text, int newCursorPosition);
1572 
nativeSetComposingText(String text, int newCursorPosition)1573     public native void nativeSetComposingText(String text, int newCursorPosition);
1574 
1575     @Override
deleteSurroundingText(int beforeLength, int afterLength)1576     public boolean deleteSurroundingText(int beforeLength, int afterLength) {
1577         // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions/14560344/android-backspace-in-webview-baseinputconnection
1578         if (beforeLength == 1 && afterLength == 0) {
1579             // backspace
1580             return super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
1581                 && super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
1582         }
1583 
1584         return super.deleteSurroundingText(beforeLength, afterLength);
1585     }
1586 }
1587 
1588 /* A null joystick handler for API level < 12 devices (the accelerometer is handled separately) */
1589 class SDLJoystickHandler {
1590 
1591     /**
1592      * Handles given MotionEvent.
1593      * @param event the event to be handled.
1594      * @return if given event was processed.
1595      */
handleMotionEvent(MotionEvent event)1596     public boolean handleMotionEvent(MotionEvent event) {
1597         return false;
1598     }
1599 
1600     /**
1601      * Handles adding and removing of input devices.
1602      */
pollInputDevices()1603     public void pollInputDevices() {
1604     }
1605 }
1606 
1607 /* Actual joystick functionality available for API >= 12 devices */
1608 class SDLJoystickHandler_API12 extends SDLJoystickHandler {
1609 
1610     static class SDLJoystick {
1611         public int device_id;
1612         public String name;
1613         public ArrayList<InputDevice.MotionRange> axes;
1614         public ArrayList<InputDevice.MotionRange> hats;
1615     }
1616     static class RangeComparator implements Comparator<InputDevice.MotionRange> {
1617         @Override
compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1)1618         public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
1619             return arg0.getAxis() - arg1.getAxis();
1620         }
1621     }
1622 
1623     private ArrayList<SDLJoystick> mJoysticks;
1624 
SDLJoystickHandler_API12()1625     public SDLJoystickHandler_API12() {
1626 
1627         mJoysticks = new ArrayList<SDLJoystick>();
1628     }
1629 
1630     @Override
pollInputDevices()1631     public void pollInputDevices() {
1632         int[] deviceIds = InputDevice.getDeviceIds();
1633         // It helps processing the device ids in reverse order
1634         // For example, in the case of the XBox 360 wireless dongle,
1635         // so the first controller seen by SDL matches what the receiver
1636         // considers to be the first controller
1637 
1638         for(int i=deviceIds.length-1; i>-1; i--) {
1639             SDLJoystick joystick = getJoystick(deviceIds[i]);
1640             if (joystick == null) {
1641                 joystick = new SDLJoystick();
1642                 InputDevice joystickDevice = InputDevice.getDevice(deviceIds[i]);
1643                 if (SDLActivity.isDeviceSDLJoystick(deviceIds[i])) {
1644                     joystick.device_id = deviceIds[i];
1645                     joystick.name = joystickDevice.getName();
1646                     joystick.axes = new ArrayList<InputDevice.MotionRange>();
1647                     joystick.hats = new ArrayList<InputDevice.MotionRange>();
1648 
1649                     List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges();
1650                     Collections.sort(ranges, new RangeComparator());
1651                     for (InputDevice.MotionRange range : ranges ) {
1652                         if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
1653                             if (range.getAxis() == MotionEvent.AXIS_HAT_X ||
1654                                 range.getAxis() == MotionEvent.AXIS_HAT_Y) {
1655                                 joystick.hats.add(range);
1656                             }
1657                             else {
1658                                 joystick.axes.add(range);
1659                             }
1660                         }
1661                     }
1662 
1663                     mJoysticks.add(joystick);
1664                     SDLActivity.nativeAddJoystick(joystick.device_id, joystick.name, 0, -1,
1665                                                   joystick.axes.size(), joystick.hats.size()/2, 0);
1666                 }
1667             }
1668         }
1669 
1670         /* Check removed devices */
1671         ArrayList<Integer> removedDevices = new ArrayList<Integer>();
1672         for(int i=0; i < mJoysticks.size(); i++) {
1673             int device_id = mJoysticks.get(i).device_id;
1674             int j;
1675             for (j=0; j < deviceIds.length; j++) {
1676                 if (device_id == deviceIds[j]) break;
1677             }
1678             if (j == deviceIds.length) {
1679                 removedDevices.add(Integer.valueOf(device_id));
1680             }
1681         }
1682 
1683         for(int i=0; i < removedDevices.size(); i++) {
1684             int device_id = removedDevices.get(i).intValue();
1685             SDLActivity.nativeRemoveJoystick(device_id);
1686             for (int j=0; j < mJoysticks.size(); j++) {
1687                 if (mJoysticks.get(j).device_id == device_id) {
1688                     mJoysticks.remove(j);
1689                     break;
1690                 }
1691             }
1692         }
1693     }
1694 
getJoystick(int device_id)1695     protected SDLJoystick getJoystick(int device_id) {
1696         for(int i=0; i < mJoysticks.size(); i++) {
1697             if (mJoysticks.get(i).device_id == device_id) {
1698                 return mJoysticks.get(i);
1699             }
1700         }
1701         return null;
1702     }
1703 
1704     @Override
handleMotionEvent(MotionEvent event)1705     public boolean handleMotionEvent(MotionEvent event) {
1706         if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) != 0) {
1707             int actionPointerIndex = event.getActionIndex();
1708             int action = event.getActionMasked();
1709             switch(action) {
1710                 case MotionEvent.ACTION_MOVE:
1711                     SDLJoystick joystick = getJoystick(event.getDeviceId());
1712                     if ( joystick != null ) {
1713                         for (int i = 0; i < joystick.axes.size(); i++) {
1714                             InputDevice.MotionRange range = joystick.axes.get(i);
1715                             /* Normalize the value to -1...1 */
1716                             float value = ( event.getAxisValue( range.getAxis(), actionPointerIndex) - range.getMin() ) / range.getRange() * 2.0f - 1.0f;
1717                             SDLActivity.onNativeJoy(joystick.device_id, i, value );
1718                         }
1719                         for (int i = 0; i < joystick.hats.size(); i+=2) {
1720                             int hatX = Math.round(event.getAxisValue( joystick.hats.get(i).getAxis(), actionPointerIndex ) );
1721                             int hatY = Math.round(event.getAxisValue( joystick.hats.get(i+1).getAxis(), actionPointerIndex ) );
1722                             SDLActivity.onNativeHat(joystick.device_id, i/2, hatX, hatY );
1723                         }
1724                     }
1725                     break;
1726                 default:
1727                     break;
1728             }
1729         }
1730         return true;
1731     }
1732 }
1733 
1734 class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
1735     // Generic Motion (mouse hover, joystick...) events go here
1736     @Override
onGenericMotion(View v, MotionEvent event)1737     public boolean onGenericMotion(View v, MotionEvent event) {
1738         float x, y;
1739         int action;
1740 
1741         switch ( event.getSource() ) {
1742             case InputDevice.SOURCE_JOYSTICK:
1743             case InputDevice.SOURCE_GAMEPAD:
1744             case InputDevice.SOURCE_DPAD:
1745                 return SDLActivity.handleJoystickMotionEvent(event);
1746 
1747             case InputDevice.SOURCE_MOUSE:
1748                 action = event.getActionMasked();
1749                 switch (action) {
1750                     case MotionEvent.ACTION_SCROLL:
1751                         x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
1752                         y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
1753                         SDLActivity.onNativeMouse(0, action, x, y);
1754                         return true;
1755 
1756                     case MotionEvent.ACTION_HOVER_MOVE:
1757                         x = event.getX(0);
1758                         y = event.getY(0);
1759 
1760                         SDLActivity.onNativeMouse(0, action, x, y);
1761                         return true;
1762 
1763                     default:
1764                         break;
1765                 }
1766                 break;
1767 
1768             default:
1769                 break;
1770         }
1771 
1772         // Event was not managed
1773         return false;
1774     }
1775 }
1776