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